# 通知設計書 88-invite_reminder_email

## 概要

本ドキュメントは、GitLabにおける「招待リマインダーメール（invite_reminder_email）」の設計を定義するものである。プロジェクトまたはグループへの招待を受けたがまだ承諾していないユーザーに対して、リマインダーメールを送信する。

### 本通知の処理概要

本通知は、メールアドレスで招待されたがまだGitLabアカウントを作成していない、または招待を承諾していないユーザーに対して、招待のリマインダーメールを送信する機能である。段階的に3回までリマインダーが送信される。

**業務上の目的・背景**：招待を見落としている、または忘れているユーザーに対してリマインダーを送信し、招待の承諾率を向上させる。チームへの参加を促進し、コラボレーションの開始を加速する。招待者の名前を表示することで、受信者に招待の重要性を認識させる。

**通知の送信タイミング**：`Member#send_invitation_reminder` メソッドで呼び出される。リマインダーは3段階（FirstEmail, SecondEmail, LastEmail）で送信され、それぞれ異なるメッセージ内容となる。

**通知の受信者**：招待先として指定されたメールアドレス（`member.invite_email`）。招待者（`created_by`）が存在し、かつ未登録ユーザー（`invite_to_unknown_user?`）の場合のみ送信される。

**通知内容の概要**：招待者の名前、プロジェクト/グループ名、付与されるロール、招待からの経過日数（LastEmailの場合）、招待承諾用リンク、および招待辞退用リンクが含まれる。

**期待されるアクション**：受信者は招待を承諾してGitLabアカウントを作成し、プロジェクト/グループに参加する。または、招待を辞退する。

## 通知種別

メール（テキスト形式）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 非同期（deliver_later） |
| 優先度 | 中 |
| リトライ | Sidekiqのデフォルトリトライ設定に従う |

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

1. Memberレコードの `invite_email` フィールドから送信先を取得
2. `member.created_by` が存在すること
3. `member.invite_to_unknown_user?` がtrueであること

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | Gitlab.config.gitlab.email_from |
| 送信元名称 | Gitlab.config.gitlab.email_display_name |
| 件名 | リマインダーインデックスにより異なる（下記参照） |
| 形式 | テキスト形式 |

### 件名パターン

| インデックス | 件名 |
|------------|------|
| 0 (FirstEmail) | `{inviter}'s invitation to GitLab is pending` |
| 1 (SecondEmail) | `{inviter} is waiting for you to join GitLab` |
| 2 (LastEmail) | `{inviter} is still waiting for you to join GitLab` |

### 本文テンプレート

**FirstEmail (index: 0):**
```
Invitation pending

{inviter} is waiting for you to join the {project_or_group_name} {project_or_group} as a {role}.

Accept invitation: {invite_url}
Decline invitation: {decline_url}
```

**SecondEmail (index: 1):**
```
Hey there!

This is a friendly reminder that {inviter} invited you to join the {project_or_group_name} {project_or_group} as a {role}.

Accept invitation: {invite_url}
Decline invitation: {decline_url}
```

**LastEmail (index: 2):**
```
In case you missed it...

It's been {invitation_age} days since {inviter} invited you to join the {project_or_group_name} {project_or_group} as a {role}. What would you like to do?

Accept invitation: {invite_url}
Decline invitation: {decline_url}
```

### 添付ファイル

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

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| inviter | 招待者の名前 | member.created_by.name | Yes |
| project_or_group_name | プロジェクト/グループ名 | member.source.human_name | Yes |
| project_or_group | ソースタイプ（project/group） | member.source.model_name.singular | Yes |
| role | 付与されるロール名 | member.human_access.downcase | Yes |
| invite_url | 招待承諾用URL | invite_url(@token) | Yes |
| decline_url | 招待辞退用URL | decline_invite_url(@token) | Yes |
| invitation_age | 招待からの経過日数（LastEmailのみ） | (Date.current - member.created_at.to_date).to_i | No |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| バッチ処理 | リマインダースケジュール | created_by存在＆invite_to_unknown_user? | 定期的なバッチ処理 |
| メソッド呼び出し | send_invitation_reminder | created_by存在＆invite_to_unknown_user? | Member#send_invitation_reminder |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| member.blank? | Memberレコードが存在しない |
| member.created_by.nil? | 招待者が存在しない |
| !member.invite_to_unknown_user? | 既存ユーザーへの招待 |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[リマインダートリガー] --> B{member.present?}
    B -->|No| C[ログ出力・終了]
    B -->|Yes| D{created_by存在?}
    D -->|No| C
    D -->|Yes| E{invite_to_unknown_user?}
    E -->|No| C
    E -->|Yes| F[トークン確認/生成]
    F --> G[email_klass選択]
    G --> H[InviteReminderMailer.email]
    H --> I[mail_with_locale]
    I --> J[deliver_later]
    J --> K[Sidekiqジョブキュー]
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| members | 招待情報取得 | invite_email, invite_token, created_by_id, created_at |
| users | 招待者情報取得 | created_by経由 |
| projects | プロジェクト情報 | source_type='Project'の場合 |
| namespaces | グループ/名前空間情報 | source_type='Namespace'の場合 |

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

#### members

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | メンバー識別 | 引数で渡される |
| invite_email | 送信先メールアドレス | - |
| invite_token | 招待トークン（暗号化済み） | - |
| created_by_id | 招待者ユーザーID | - |
| created_at | 招待作成日時 | LastEmailで経過日数計算用 |
| source_id | ソースID | - |
| source_type | ソースタイプ | - |
| access_level | アクセスレベル | ロール表示用 |
| user_id | ユーザーID | NULL（未登録）であること |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| members | UPDATE | トークン再生成時にinvite_tokenを更新 |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| メンバー未検出 | member.blank? | ログ出力してスキップ |
| SMTP接続エラー | メールサーバー接続失敗 | Sidekiqリトライ |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | Sidekiqデフォルト（25回） |
| リトライ間隔 | 指数バックオフ |
| リトライ対象エラー | SMTPConnectionError等のネットワークエラー |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | Sidekiqキューの設定に依存 |
| 1日あたり上限 | 特に制限なし |

### 配信時間帯

バッチ処理のスケジュールに依存

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

- 招待トークンはDevise.token_generatorで暗号化されて保存
- メール本文には生のトークンが含まれる
- 承諾・辞退用URLへのアクセスには認証不要だが、トークンの知識が必要

## 備考

- `email_klass` で3つのメールクラス（FirstEmail, SecondEmail, LastEmail）を使い分け
- `reminder_index` (0, 1, 2) でどのリマインダーかを指定
- `invite_to_unknown_user?` は `invite? && user_id.nil?` で判定
- レイアウトは `unknown_user_mailer` が使用される
- SecondEmailではHTML版で絵文字（wave）が使用される

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | member.rb | `app/models/member.rb` | invite_to_unknown_user?（647-649行目）を確認 |

**読解のコツ**: `invite_to_unknown_user?` は `invite? && user_id.nil?` を返す。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | member.rb | `app/models/member.rb` | send_invitation_reminder（604-612行目）を確認 |

**主要処理フロー**:
1. **605行目**: invite?チェック
2. **607行目**: トークン確認/生成
3. **609-611行目**: InviteReminderMailer呼び出し

#### Step 3: メーラーを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | invite_reminder_mailer.rb | `app/mailers/members/invite_reminder_mailer.rb` | emailメソッド（13-28行目）を確認 |

**主要処理フロー**:
- **13-16行目**: 引数の設定
- **18行目**: valid_to_email?チェック
- **20行目**: email_klass選択
- **22行目**: 件名生成

#### Step 4: メールクラスを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | invite_reminder_mailer.rb | `app/mailers/members/invite_reminder_mailer.rb` | FirstEmail, SecondEmail, LastEmail（58-133行目）を確認 |

#### Step 5: テンプレートを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | email.text.erb | `app/views/members/invite_reminder_mailer/email.text.erb` | テキストテンプレートの内容確認 |

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

```
Member#send_invitation_reminder(reminder_index)
    │
    ├─ invite? チェック
    │
    ├─ generate_invite_token! (必要な場合)
    │
    └─ run_after_commit_or_now
           │
           └─ Members::InviteReminderMailer.email(member, raw_token, reminder_index)
                  │
                  ├─ valid_to_email? チェック
                  │      └─ created_by存在 && invite_to_unknown_user?
                  │
                  ├─ email_klass[reminder_index]
                  │      ├─ 0: FirstEmail
                  │      ├─ 1: SecondEmail
                  │      └─ 2: LastEmail
                  │
                  └─ mail_with_locale → deliver_later
```

### データフロー図

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

リマインダー実行  ───▶ send_invitation_reminder    ───▶ トークン確認/生成
    │                      │
    ├─ member              ├─▶ email_klass選択
    └─ reminder_index      │      │
                           │      ├─▶ FirstEmail (index: 0)
                           │      ├─▶ SecondEmail (index: 1)
                           │      └─▶ LastEmail (index: 2)
                           │
                           └─▶ メール送信（Sidekiq）
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| member.rb | `app/models/member.rb` | ソース | Memberモデル、send_invitation_reminder定義 |
| invite_reminder_mailer.rb | `app/mailers/members/invite_reminder_mailer.rb` | ソース | 招待リマインダーメーラー |
| email.text.erb | `app/views/members/invite_reminder_mailer/email.text.erb` | テンプレート | テキスト形式メールテンプレート |
| unknown_user_mailer.html.haml | `app/views/layouts/unknown_user_mailer.html.haml` | レイアウト | メールレイアウト |
