# 通知設計書 83-invite_initial_email

## 概要

本ドキュメントは、GitLabにおける「招待メール（invite_initial_email）」の設計を定義するものである。プロジェクトまたはグループにユーザーをメールアドレスで招待した際に、招待を受けたメールアドレスに送信される初回招待メールである。

### 本通知の処理概要

本通知は、プロジェクトまたはグループにユーザーをメールアドレスで招待した際に、そのメールアドレス宛に招待通知を送信する機能である。GitLabアカウントを持たないユーザーも招待可能であり、招待メールを通じてアカウント作成と参加を促す。

**業務上の目的・背景**：チームやプロジェクトへの新規メンバー招待を可能にし、コラボレーションの拡大を支援する。特に、まだGitLabアカウントを持たない外部メンバーを招待する際に重要な機能である。招待者の名前を表示することで、受信者が誰からの招待かを認識でき、不審な招待との区別が可能になる。

**通知の送信タイミング**：メンバー招待が作成された直後に送信される。招待は `Member#after_create :send_invite` コールバックでトリガーされる。また、招待の再送信時にも `Member#resend_invite` で送信される。

**通知の受信者**：招待先として指定されたメールアドレス（`member.invite_email`）。既存GitLabユーザーまたは未登録のメールアドレスのいずれも対象となる。

**通知内容の概要**：招待者の名前（設定されている場合）、プロジェクト/グループ名、付与されるロール、ロールタイプ、および招待承諾用のリンクが含まれる。

**期待されるアクション**：受信者は招待メールを確認し、「Join now」リンクをクリックしてGitLabにアクセスする。未登録ユーザーの場合はアカウント作成後に招待が承諾され、既存ユーザーの場合はログイン後に承諾される。

## 通知種別

メール（テキスト形式）

## 送信仕様

### 基本情報

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

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

1. Memberレコードの `invite_email` フィールドから送信先を取得
2. `invite_email` は招待作成時に指定されたメールアドレス
3. 既存ユーザーのメールアドレスでも、未登録のメールアドレスでも送信可能

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | Gitlab.config.gitlab.email_from |
| 送信元名称 | Gitlab.config.gitlab.email_display_name |
| 件名 | `{inviter_name} invited you to join GitLab` または `Invitation to join the {project_or_group} {project_or_group_name}` |
| 形式 | テキスト形式 |

### 本文テンプレート

**招待者がいる場合：**
```
{inviter} invited you to join the {project_or_group_name} {project_or_group} with the following role: {role}.
This is a {role_type} role.

Join now: {invite_url}
```

**招待者がいない場合：**
```
You have been invited to join the {project_or_group_name} {project_or_group} with the following role: {role}.
This is a {role_type} role.

Join now: {invite_url}
```

### 添付ファイル

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

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| inviter | 招待者の名前 | member.created_by.name | No（created_byがある場合のみ） |
| project_or_group_name | プロジェクト/グループ名 | member_source.human_name | Yes |
| project_or_group | ソースタイプ（project/group） | member_source.model_name.singular | Yes |
| role | 付与されるロール名 | member.present.human_access | Yes |
| role_type | ロールタイプ | member.present.role_type | Yes |
| invite_url | 招待承諾用URL | invite_url(@token) | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 画面操作 | メンバー招待 | invite_emailが設定されている | メンバー管理画面から招待 |
| API | Member作成 | invite_emailが設定されている | API経由での招待 |
| 画面操作 | 招待再送信 | 招待トークンが有効 | 招待の再送信操作 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| member.blank? | Memberレコードが存在しない |
| importing? | インポート処理中 |
| invite?がfalse | 招待状態でない（invite_tokenがない） |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[Member作成/招待再送信] --> B{invite?}
    B -->|No| C[送信スキップ]
    B -->|Yes| D{importing?}
    D -->|Yes| C
    D -->|No| E[send_invite / resend_invite]
    E --> F{member.present?}
    F -->|No| G[ログ出力・終了]
    F -->|Yes| H[トークン生成/確認]
    H --> I[Tracking.event送信]
    I --> J[InviteMailer.initial_email]
    J --> K[deliver_later]
    K --> L[Sidekiqジョブキュー]
    L --> M[メール送信完了]
```

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

### 参照テーブル一覧

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

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

#### members

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | メンバー識別 | 引数で指定 |
| invite_email | 送信先メールアドレス | - |
| invite_token | 招待トークン（暗号化済み） | - |
| created_by_id | 招待者ユーザーID | - |
| source_id | ソースID | - |
| source_type | ソースタイプ | - |
| access_level | アクセスレベル | ロール表示用 |

#### users

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | 招待者ユーザー識別 | members.created_by_id |
| name | 招待者名 | 件名・本文表示用 |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| members | UPDATE | resend_invite時にinvite_tokenを再生成 |

#### members（resend_invite時）

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|-------|------|
| UPDATE | invite_token | 新規トークン（暗号化） | generate_invite_token!による |

## エラー処理

### エラーケース一覧

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

### リトライ仕様

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

## 配信設定

### レート制限

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

### 配信時間帯

特に制限なし（招待作成時に即座に送信）

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

- 招待トークンはDevise.token_generatorで暗号化されて保存
- メール本文には生のトークン（@raw_invite_token）が含まれる
- トークンURLへのアクセスには認証が必要（未登録者はアカウント作成が必要）
- Mailgun連携が有効な場合、トラッキング用のカスタムヘッダーが追加される

## 備考

- `Gitlab::Tracking.event` でメール送信がトラッキングされる（label: 'invite_email'）
- Mailgun連携時は `X-Mailgun-Tag` と `X-Mailgun-Variables` ヘッダーが追加される
- 招待先の説明文（description）はプロジェクト/グループの説明またはデフォルト説明文が使用される
- レイアウトは `unknown_user_mailer` が使用される（未登録ユーザー向け）

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | member.rb | `app/models/member.rb` | 招待関連のフィールド（invite_email, invite_token, created_by）を確認 |

**読解のコツ**: `invite?` メソッドは `invite_token.present?` を返す。招待状態のメンバーは `user_id` がnilで `invite_token` が設定されている。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | member.rb | `app/models/member.rb` | send_invite（704-705行目）、resend_invite（596-602行目）を確認 |

**主要処理フロー**:
1. **364行目**: after_create :send_invite, if: :invite?
2. **704-705行目**: send_invite メソッド - run_after_commit_or_now でメーラー呼び出し
3. **596-602行目**: resend_invite メソッド - トークン再生成とsend_invite呼び出し

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

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

**主要処理フロー**:
- **16-17行目**: @member, @tokenのセット
- **20行目**: member_exists?チェック
- **22行目**: Tracking.eventでトラッキング
- **24-28行目**: mail_with_locale呼び出し

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

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

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

```
Member#after_create (if: :invite?)
    │
    └─ send_invite
           │
           └─ run_after_commit_or_now
                  │
                  └─ Members::InviteMailer.initial_email(member, raw_invite_token)
                         │
                         ├─ member_exists? チェック
                         │
                         ├─ Gitlab::Tracking.event
                         │
                         └─ mail_with_locale → deliver_later

Member#resend_invite
    │
    ├─ invite? チェック
    │
    ├─ generate_invite_token! (必要な場合)
    │
    └─ send_invite
```

### データフロー図

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

Member作成（招待） ───▶ after_create :send_invite   ───▶ メール送信（Sidekiq）
    │                      │
    ├─ invite_email        ├─▶ InviteMailer.initial_email
    ├─ created_by_id       │      │
    └─ raw_invite_token    │      ├─▶ @member
                           │      ├─▶ @token
                           │      └─▶ email_subject_text
                           │
                           └─▶ Tracking.event
```

### 関連ファイル一覧

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