# 通知設計書 4-reassigned_issue_email

## 概要

本ドキュメントは、GitLabにおけるIssueの担当者変更通知メール機能（reassigned_issue_email）の設計仕様を定義する。

### 本通知の処理概要

reassigned_issue_emailは、GitLabプロジェクト内のIssueの担当者（assignee）が変更された際に、関連するユーザーに対してメール通知を送信する機能である。

**業務上の目的・背景**：プロジェクト管理において、タスクの担当者の変更は重要なイベントである。以前の担当者は自分が担当から外れたことを知る必要があり、新しい担当者は新たなタスクが割り当てられたことを認識する必要がある。この通知により、タスクの引き継ぎがスムーズに行われ、作業の抜け漏れを防止する。

**通知の送信タイミング**：Issueの担当者が追加、削除、または変更された直後に、非同期（Sidekiq経由）でメール送信がトリガーされる。NotificationService.new.reassigned_issue(issue, current_user, previous_assignees)が呼び出されることで処理が開始される。

**通知の受信者**：以前の担当者（previous_assignees）、新しい担当者、カスタム通知設定で「reassign issue」を有効にしているユーザーが受信対象となる。以前の担当者と新しい担当者の両方が通知を受け取る。

**通知内容の概要**：追加された担当者名、削除された担当者名が含まれる。「{name} was added as an assignee.」「{name} was removed as an assignee.」「All assignees were removed.」といったメッセージが表示される。

**期待されるアクション**：新しい担当者はIssueの内容を確認して作業を開始し、以前の担当者は引き継ぎ作業や情報提供を行うことが期待される。

## 通知種別

メール通知

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 非同期（Sidekiq deliver_later） |
| 優先度 | 中 |
| リトライ | Sidekiqデフォルト設定（最大25回） |

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

NotificationRecipients::BuildServiceを使用して受信者を決定する。以下の特徴がある：

1. action: "reassign"で受信者を構築
2. previous_assigneesオプションで以前の担当者を指定
3. 以前の担当者には:mentionレベルで通知
4. 新しい担当者にはNotificationReason::ASSIGNEDとして通知
5. 現在のユーザー（変更者）は通知対象から除外される（skip_current_user: true）

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | `{sender.name} ({sender.to_reference})` <gitlab@{host}> |
| 送信元名称 | 変更者名（@ユーザー名） |
| 件名 | `Re: {project_name} | {issue_title} (#{iid})` |
| 形式 | HTML/テキスト両対応（multipart） |

### 本文テンプレート

```
{added_names} was added as an assignee. （追加された担当者がいる場合）
{added_names} were added as assignees. （複数追加の場合）

{removed_names} was removed as an assignee. （削除された担当者がいる場合、かつ新担当者がいる場合）
{removed_names} were removed as assignees. （複数削除の場合）

All assignees were removed. （全担当者が削除された場合）
```

### 添付ファイル

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

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| @issue | Issueオブジェクト | Issue.find(issue_id) | Yes |
| @project | プロジェクトオブジェクト | @issue.project | Yes |
| @namespace | 名前空間 | @issue.namespace | Yes |
| @target_url | IssueへのURL | Gitlab::UrlBuilder.build(@issue) | Yes |
| @recipient | 受信者ユーザー | User.find(recipient_id) | Yes |
| @sent_notification | 送信通知記録 | SentNotification.record | Yes |
| @added_assignees | 追加された担当者名リスト | 計算値 | Yes |
| @removed_assignees | 削除された担当者名リスト | 計算値 | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 画面操作 | Issue編集画面での担当者変更 | 担当者リストに変更あり | WebUIからの担当者変更 |
| API | PUT /api/v4/projects/:id/issues/:issue_iid | assignee_idsの変更 | REST API経由の担当者変更 |
| クイックアクション | /assign, /unassign コマンド | コマンド実行成功 | コメント経由の担当者変更 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| project.emails_disabled? == true | プロジェクトでメール通知が無効化されている場合 |
| 受信者の通知レベルがDisabled | ユーザー設定で通知を無効化している場合 |
| レート制限超過 | プロジェクト/グループ単位の通知レート制限に達した場合 |
| 担当者変更なし | 同じ担当者リストで更新した場合 |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[Issue担当者変更] --> B[NotificationService.reassigned_issue]
    B --> C[NotificationRecipients::BuildService.build_recipients]
    C --> D[action: reassign, previous_assignees指定]
    D --> E[受信者リスト構築]
    E --> F[previous_assigneeから現担当者差分計算]
    F --> G[@added_assignees, @removed_assignees計算]
    G --> H[各受信者に対してループ]
    H --> I[Notify.reassigned_issue_email.deliver_later]
    I --> J[Sidekiqキューに追加]
    J --> K[setup_issue_mail実行]
    K --> L[mail_answer_thread実行]
    L --> M[メール送信]
    M --> N[終了]
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| issues | Issue情報取得 | メール本文データ |
| users | ユーザー情報取得 | 作成者・受信者・担当者 |
| issue_assignees | 担当者情報 | 現在の担当者取得 |
| projects | プロジェクト情報 | メール件名・ヘッダー |
| namespaces | 名前空間情報 | グループ情報取得 |
| notification_settings | 通知設定 | 受信者フィルタリング |

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

#### issues

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | Issue識別 | PRIMARY KEY |
| iid | Issue番号 | メール件名に使用 |
| title | タイトル | メール件名に使用 |
| project_id | プロジェクトID | プロジェクト情報取得 |
| confidential | 機密フラグ | ヘッダー設定 |

#### issue_assignees

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| issue_id | Issue ID | Issue識別 |
| user_id | 担当者ID | 現在の担当者取得 |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| p_sent_notifications | INSERT | 送信通知記録 |

#### 送信ログテーブル（p_sent_notifications）

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|-------|------|
| INSERT | project_id | @issue.project_id | プロジェクトID |
| INSERT | recipient_id | recipient_id | 受信者ID |
| INSERT | reply_key | SecureRandom生成 | 返信キー |
| INSERT | noteable_type | 'Issue' | 通知対象タイプ |
| INSERT | noteable_id | @issue.id | Issue ID |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 送信失敗 | SMTP接続エラー | Sidekiqリトライ |
| テンプレートエラー | Hamlレンダリング失敗 | エラーログ出力 |
| 宛先不正 | メールアドレス形式不正 | 送信スキップ |
| Issue削除済み | 送信前にIssueが削除 | RecordNotFound例外 |
| ユーザー削除済み | 担当者ユーザーが削除 | 該当ユーザースキップ |

### リトライ仕様

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

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | Gitlab::ApplicationRateLimiter設定による |
| 1日あたり上限 | プロジェクト/グループ単位で設定 |

### 配信時間帯

制限なし（24時間送信可能）

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

- 機密Issue（confidential: true）の場合、X-GitLab-ConfidentialIssueヘッダーがtrueに設定される
- 受信者が当該Issueへのアクセス権限を持つかの確認はNotificationRecipientクラスで実施
- メール本文に含まれる個人情報（担当者名）は必要最小限

## 備考

- previous_assignee_idsはUser.where(id: previous_assignee_ids).order(:id)で取得される
- @added_assigneesは@issue.assignees.map(&:name) - previous_assignees.map(&:name)で計算
- @removed_assigneesはprevious_assignees.map(&:name) - @issue.assignees.map(&:name)で計算
- 複数形/単数形の表現はn_()関数で国際化対応
- テンプレートは_reassigned_issuable_email.html.hamlを使用（Issue/MergeRequest共通）

---

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

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

### 推奨読解順序

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

まず、担当者関連のデータ構造を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | issue.rb | `app/models/issue.rb` | has_many :issue_assignees、has_many :assignees（91-96行目） |
| 1-2 | issue_assignee.rb | `app/models/issue_assignee.rb` | 担当者関連テーブルの定義 |

**読解のコツ**: Issueとassigneesの関連（has_many :through）を理解することで、担当者変更のデータフローが把握できる。

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

処理の起点となるNotificationServiceのreassigned_issueメソッドを確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | notification_service.rb | `app/services/notification_service.rb` | reassigned_issueメソッド（243-263行目）がエントリーポイント |

**主要処理フロー**:
1. **243行目**: `def reassigned_issue(issue, current_user, previous_assignees = [])` - 公開メソッド定義
2. **244-249行目**: BuildService.build_recipients呼び出し（action: "reassign", previous_assignees指定）
3. **251行目**: previous_assignee_ids計算
4. **253-261行目**: 各受信者にreassigned_issue_emailを送信（previous_assignee_ids引数付き）

#### Step 3: 受信者決定ロジックを理解する

reassignアクション時の受信者構築処理を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | default.rb | `app/services/notification_recipients/builder/default.rb` | build!メソッド内のreassign処理（33-36行目） |

**主要処理フロー**:
- **34行目**: `add_recipients(previous_assignees, :mention, nil)` - 以前の担当者追加
- **35行目**: `add_recipients(target.assignees, :mention, NotificationReason::ASSIGNED)` - 新担当者追加

#### Step 4: メーラー実装を理解する

実際のメール生成処理を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | issues.rb | `app/mailers/emails/issues.rb` | reassigned_issue_emailメソッド（37-53行目） |

**主要処理フロー**:
- **37行目**: 5引数（recipient_id, issue_id, previous_assignee_ids, updated_by_user_id, reason）
- **40-43行目**: previous_assigneesをUser.where(id:).order(:id)で取得
- **42行目**: @added_assignees計算（現在 - 以前）
- **43行目**: @removed_assignees計算（以前 - 現在）
- **45-52行目**: mail_answer_thread実行

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

メール本文のレンダリング処理を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | _reassigned_issuable_email.html.haml | `app/views/notify/_reassigned_issuable_email.html.haml` | 共通テンプレート（全13行） |

**主要処理フロー**:
- **1-2行目**: added_names, removed_namesの生成（to_sentence使用）
- **4-6行目**: 担当者追加時のメッセージ（n_関数で複数形対応）
- **7-9行目**: 担当者削除時のメッセージ（新担当者がいる場合）
- **10-12行目**: 全担当者削除時のメッセージ

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

```
Issues::UpdateService
    │
    └─ NotificationService#reassigned_issue (notification_service.rb:243)
           │
           ├─ NotificationRecipients::BuildService.build_recipients
           │      │
           │      └─ Builder::Default#build!
           │             ├─ action: "reassign"
           │             ├─ add_recipients(previous_assignees, :mention, nil)
           │             └─ add_recipients(target.assignees, :mention, ASSIGNED)
           │
           ├─ previous_assignee_ids = previous_assignees.map(&:id)
           │
           └─ recipients.each { Notify.reassigned_issue_email.deliver_later }
                  │
                  └─ Notify#reassigned_issue_email (emails/issues.rb:37)
                         │
                         ├─ #setup_issue_mail
                         │
                         ├─ User.where(id: previous_assignee_ids).order(:id)
                         │
                         ├─ @added_assignees = current - previous
                         │
                         ├─ @removed_assignees = previous - current
                         │
                         └─ #mail_answer_thread
```

### データフロー図

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

Issue担当者変更 ────────────▶ Issues::UpdateService ─────────▶ previous_assignees
     │                            │                              │
     │                            ▼                              │
     │                  NotificationService                      │
     │                  #reassigned_issue                        │
     │                            │                              │
     │                            ▼                              │
     │                     BuildService ────────────────────────▶ Sidekiqキュー
     │                     (受信者決定)                              │
     │                            │                              │
     └──────────────────▶ Emails::Issues ─────────────────────────┤
                          #reassigned_issue_email                │
                                │                                │
                                ├─ @added_assignees計算           │
                                ├─ @removed_assignees計算         │
                                │                                │
                                ▼                                ▼
                        SentNotification ──────────▶ p_sent_notifications
                        (記録作成)                    テーブル
                                │
                                ▼
                        ActionMailer ────────────────▶ SMTPサーバー
                        (メール送信)                   └─▶ 新旧担当者メールボックス
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| notification_service.rb | `app/services/notification_service.rb` | サービス | 通知処理のエントリーポイント（243-263行目） |
| issues.rb | `app/mailers/emails/issues.rb` | メーラー | reassigned_issue_emailメソッド定義（37-53行目） |
| notify.rb | `app/mailers/notify.rb` | メーラー | 共通メール処理・ヘッダー設定 |
| _reassigned_issuable_email.html.haml | `app/views/notify/_reassigned_issuable_email.html.haml` | テンプレート | 担当者変更通知共通テンプレート |
| default.rb | `app/services/notification_recipients/builder/default.rb` | サービス | reassignアクション時の受信者構築（33-36行目） |
| issue.rb | `app/models/issue.rb` | モデル | Issueデータモデル（担当者関連） |
| issue_assignee.rb | `app/models/issue_assignee.rb` | モデル | Issue担当者関連テーブル |
