# 通知設計書 2-会員サインアップメール

## 概要

本ドキュメントは、Ghost CMSにおける会員サインアップメール（新規無料会員登録確認メール）の設計仕様を記載する。新規会員がサイトに登録を要求した際に送信される確認メールの送信ロジック、テンプレート構造、およびデータフローを定義する。

### 本通知の処理概要

本通知は、新規ユーザーがGhostサイトへの会員登録を要求した際に、メールアドレスの確認とアカウント作成を完了するためのマジックリンクを含むメールを送信する機能である。

**業務上の目的・背景**：新規会員登録時のメールアドレス検証を行い、不正なアカウント作成（なりすまし、使い捨てメール等）を防止する。また、ダブルオプトイン形式を採用することで、GDPRなどのプライバシー規制に準拠した会員登録プロセスを実現する。

**通知の送信タイミング**：新規ユーザーがサイトの登録フォーム（Portal等）でメールアドレスを入力し、サインアップボタンをクリックした時点でトリガーされる。既存会員のメールアドレスが入力された場合は、自動的にサインインメールにフォールバックする。

**通知の受信者**：サインアップを要求したユーザーのメールアドレスに送信される。このメールアドレスはまだmembersテーブルに登録されていない状態である。

**通知内容の概要**：メールにはサインアップ完了用のマジックリンクURLが含まれる。リンクをクリックすることでメールアドレスの検証が完了し、会員アカウントが作成される。同時に自動ログインも行われる。リンクは24時間で有効期限が切れる。

**期待されるアクション**：受信者はメール内の「Confirm signup」ボタンをクリックしてサインアップを完了する。リンクをクリックすると、会員アカウントが作成され、自動的にログイン状態となる。

## 通知種別

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

## 送信仕様

### 基本情報

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

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

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

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | サイト設定の`members_support_address`または`noreply@[サイトドメイン]` |
| 送信元名称 | サイトタイトル（settingsCache.get('title')） |
| 件名 | `Complete your sign up to {siteTitle}!` |
| 形式 | HTML + テキスト（マルチパート） |

### 本文テンプレート

```html
件名: Complete your sign up to {siteTitle}!

本文:
Hey there!

Tap the link below to complete the signup process for {siteTitle}, and be automatically signed in:

[Confirm signup ボタン]

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 simply delete this message.
You will not be signed up, and no account will be created for you.

This message was sent from {siteDomain} to {email}.
```

### 添付ファイル

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

## テンプレート変数

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

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| API呼び出し | POST /members/api/send-magic-link | emailType='signup' かつ会員が存在しない | 新規会員登録フォームからの送信 |
| API呼び出し | POST /members/api/send-magic-link | emailType='subscribe' かつ会員が存在しない | 購読フォームからの新規登録 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| 会員登録済み | 指定メールアドレスがmembersテーブルに既存の場合はサインインメールにフォールバック |
| 無効なメールアドレス | isEmail()バリデーションを通過しない場合 |
| ハニーポット検出 | honeypotフィールドに値がある場合（ボット対策） |
| ブロックドメイン | `all_blocked_email_domains`設定でブロックされたドメインの場合 |
| 招待制限 | members_signup_access が 'invite' の場合（管理者招待のみ許可） |
| 有料会員のみ | members_signup_access が 'paid' の場合（有料プランのみ許可） |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[POST /members/api/send-magic-link] --> B[メールアドレス正規化]
    B --> C{バリデーション}
    C -->|失敗| D[400 Bad Request]
    C -->|成功| E{セルフサインアップ許可?}
    E -->|No| F[招待制限/有料制限エラー]
    E -->|Yes| G{ブロックドメイン?}
    G -->|Yes| H[ドメインブロックエラー]
    G -->|No| I{会員存在確認}
    I -->|存在する| J[サインインメールにフォールバック]
    I -->|存在しない| K[トークン生成]
    K --> L[マジックリンクURL生成]
    L --> M[HTMLテンプレート展開]
    M --> N[メール送信]
    N --> O{送信結果}
    O -->|成功| P[201 Created + sniperLinks]
    O -->|失敗| Q[エラーレスポンス]
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| members | 会員存在確認 | email列で検索、存在時はサインインにフォールバック |
| settings | サイト設定取得 | settingsCacheから取得 |

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

#### members

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| email | 会員存在確認 | WHERE email = [入力メールアドレス] |

### 更新テーブル一覧

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

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

本機能では明示的な送信ログテーブルへの書き込みは行われない。

**注意**: 会員レコード（members）への書き込みは、このメール送信時点では行われない。会員レコードは、ユーザーがマジックリンクをクリックしてサインアップを完了した時点で作成される。

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| BadRequestError | メールアドレスが未入力または不正形式 | 400エラーを返却 |
| BadRequestError | 招待制限中（members_signup_access='invite'） | エラーメッセージ「This site is invite-only」を返却 |
| BadRequestError | 有料限定中（members_signup_access='paid'） | エラーメッセージ「This site only accepts paid members」を返却 |
| BadRequestError | ブロックドメイン | エラーメッセージ「Signups from this email domain are currently restricted」を返却 |
| EENVELOPE | メール送信時のエンベロープエラー | 400エラーを返却 |

### リトライ仕様

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

## 配信設定

### レート制限

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

### 配信時間帯

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

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

- マジックリンクトークンは24時間で有効期限切れ
- トークンは暗号学的にセキュアな方法で生成
- ハニーポットフィールドによるボット対策
- メールアドレスの正規化により、ホモグラフ攻撃を軽減
- ブロックドメインリストによる使い捨てメール対策
- セルフサインアップの制限設定（招待制/有料限定）による不正登録防止
- トークンにはIPアドレス情報が含まれ、ジオロケーション取得に使用される

## 備考

- サインアップ完了時に、トークンに含まれるname, labels, newslettersの情報が会員レコードに反映される
- attribution（帰属情報）もトークンに含まれ、会員作成時に参照される
- 既存会員がサインアップを試みた場合、自動的にサインインフローにフォールバックする
- subscribeタイプのリクエストでも、会員が存在しない場合はsignupとして処理される

---

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

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

### 推奨読解順序

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

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | signup.js | `ghost/core/core/server/services/members/emails/signup.js` | テンプレート関数のシグネチャ（t, siteTitle, email, url等）を確認。サインインテンプレートとの違いを把握 |

**読解のコツ**: signup.jsはsignin.jsと異なりOTCを受け取らないシンプルな構造。「Confirm signup」ボタンのテキストに注目。

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

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | router-controller.js | `ghost/core/core/server/services/members/members-api/controllers/router-controller.js` | _handleSignup()メソッド（823-855行目）がサインアップ処理の実装 |

**主要処理フロー**:
1. **824-834行目**: セルフサインアップ許可チェック
2. **836-842行目**: ブロックドメインチェック
3. **846-852行目**: トークンデータの構築（labels, name, newsletters, attribution等）
4. **854行目**: sendEmailWithMagicLink呼び出し

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

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | members-api.js | `ghost/core/core/server/services/members/members-api/members-api.js` | sendEmailWithMagicLink()関数でのtype判定ロジック（225-232行目） |
| 3-2 | magic-link.js | `ghost/core/core/server/services/lib/magic-link/magic-link.js` | typeが'signup'の場合のテンプレート選択 |

**主要処理フロー**:
- **225-232行目**: 会員存在確認とtypeの決定（存在しなければsignup）
- **233行目**: magicLinkService.sendMagicLink()呼び出し

#### Step 4: 会員作成フローを理解する

サインアップ完了時の会員作成処理を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | members-api.js | `ghost/core/core/server/services/members/members-api/members-api.js` | getMemberDataFromMagicLinkToken()（254-294行目）で会員作成 |

**主要処理フロー**:
- **272-277行目**: typeがsignup以外なら会員作成しない
- **290行目**: users.create()で会員レコード作成

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

```
POST /members/api/send-magic-link (emailType='signup')
    │
    ├─ RouterController.sendMagicLink()
    │      │
    │      ├─ normalizeEmail() - メールアドレス正規化
    │      │
    │      ├─ _handleSignup()
    │      │      │
    │      │      ├─ _allowSelfSignup() - セルフサインアップ許可チェック
    │      │      │
    │      │      ├─ ブロックドメインチェック
    │      │      │
    │      │      └─ sendEmailWithMagicLink()
    │      │             │
    │      │             ├─ memberRepository.get() - 会員存在確認
    │      │             │
    │      │             └─ MagicLink.sendMagicLink()
    │      │                    │
    │      │                    ├─ tokenProvider.create()
    │      │                    │
    │      │                    ├─ getSigninURL()
    │      │                    │
    │      │                    └─ transporter.sendMail()
    │      │                           │
    │      │                           └─ signup.js テンプレート関数
    │
    └─ getSniperLinks()

--- マジックリンククリック後 ---

GET /members/api/session (token付き)
    │
    └─ getMemberDataFromMagicLinkToken()
           │
           ├─ tokenProvider.validate()
           │
           └─ users.create() - 会員レコード作成
                  │
                  └─ name, email, labels, newsletters, attribution, geolocationを設定
```

### データフロー図

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

リクエストボディ ───────▶ RouterController
  - email                      │
  - emailType                  ▼
  - name                  メールアドレス正規化
  - labels                     │
  - newsletters                ▼
  - urlHistory            セルフサインアップ許可チェック
                               │
                               ▼
membersテーブル ◀────── 会員存在確認（存在しないことを確認）
                               │
                               ▼
                         トークン生成
                         （name, labels, newsletters,
                           attribution, reqIp含む）
                               │
settings ─────────────▶  テンプレート展開
  - title                      │
  - accent_color               ▼
                         メール送信 ─────────▶ ユーザーのメールボックス
                               │
                               ▼
                         APIレスポンス
                           - sniperLinks
```

### 関連ファイル一覧

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