# 通知設計書 10-issue_moved_email

## 概要

本ドキュメントは、GitLabにおけるIssueの移動通知メール機能（issue_moved_email）の設計仕様を定義する。

### 本通知の処理概要

issue_moved_emailは、GitLabプロジェクト内のIssueが別のプロジェクトに移動された際に、関連するユーザーに対してメール通知を送信する機能である。

**業務上の目的・背景**：プロジェクト管理において、Issueの組織変更やプロジェクト再編に伴い、Issueを別のプロジェクトに移動する必要が生じることがある。この移動により、元のIssueを追跡していた関係者が新しいIssueの場所を見失う可能性がある。この通知により、関係者がIssueの移動を認識し、新しいプロジェクトでの追跡を継続できる。

**通知の送信タイミング**：Issueが別のプロジェクトに移動された直後に、非同期（Sidekiq経由）でメール送信がトリガーされる。NotificationService.new.issue_moved(issue, new_issue, current_user)が呼び出されることで処理が開始される。

**通知の受信者**：元のIssueの参加者、プロジェクトのウォッチャー、カスタム通知設定ユーザー、購読者が受信対象となる。移動を実行したユーザーは通知対象から除外される。

**通知内容の概要**：「Issue was moved to another project.」というメッセージと、新しいIssueへのリンク（アクセス権がある場合）または「You don't have access to the project.」というメッセージが含まれる。

**期待されるアクション**：受信者はIssueの移動を確認し、新しいプロジェクトでIssueの追跡を継続するか、必要に応じて新しいプロジェクトへのアクセス権をリクエストすることが期待される。

## 通知種別

メール通知

## 送信仕様

### 基本情報

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

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

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

1. action: 'moved'で受信者を構築
2. 元のIssueの参加者（participants）に通知
3. プロジェクトのウォッチャーに通知
4. カスタム通知設定ユーザーに通知
5. 購読者に通知
6. 現在のユーザー（移動実行者）は通知対象から除外される（skip_current_user: true）

## 通知テンプレート

### メール通知の場合

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

### 本文テンプレート

```
Issue was moved to another project.

New issue: {new_project_issue_url} （アクセス権がある場合）

You don't have access to the project. （アクセス権がない場合）
```

### 添付ファイル

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

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| @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（引数） | Yes |
| @sent_notification | 送信通知記録 | SentNotification.record | Yes |
| @new_issue | 新しいIssueオブジェクト | 引数から取得 | Yes |
| @new_project | 新しいプロジェクト | @new_issue.project | Yes |
| @can_access_project | アクセス権フラグ | recipient.can?(:read_project, @new_project) | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 画面操作 | Issue移動ダイアログで移動実行 | 移動成功 | WebUIからのIssue移動 |
| API | POST /api/v4/projects/:id/issues/:issue_iid/move | 移動成功 | REST API経由のIssue移動 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| project.emails_disabled? == true | プロジェクトでメール通知が無効化されている場合 |
| 受信者の通知レベルがDisabled | ユーザー設定で通知を無効化している場合 |
| レート制限超過 | プロジェクト/グループ単位の通知レート制限に達した場合 |
| 移動失敗 | Issue移動が失敗した場合 |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[Issue移動実行] --> B[NotificationService.issue_moved]
    B --> C[NotificationRecipients::BuildService.build_recipients]
    C --> D[action: moved]
    D --> E[受信者リスト構築]
    E --> F[各受信者に対してループ]
    F --> G[Notify.issue_moved_email.deliver_later]
    G --> H[Sidekiqキューに追加]
    H --> I[setup_issue_mail実行]
    I --> J[@new_issue設定]
    J --> K[@new_project設定]
    K --> L[@can_access_project判定]
    L --> M[mail_answer_thread実行]
    M --> N[メール送信]
    N --> O[終了]
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| issues | Issue情報取得 | 元・新両方のIssue |
| users | ユーザー情報取得 | 作成者・受信者・移動実行者 |
| projects | プロジェクト情報 | 元・新両方のプロジェクト |
| namespaces | 名前空間情報 | グループ情報取得 |
| notification_settings | 通知設定 | 受信者フィルタリング |
| project_authorizations | プロジェクト権限 | アクセス権確認 |

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

#### issues

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

### 更新テーブル一覧

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

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 送信失敗 | SMTP接続エラー | Sidekiqリトライ |
| テンプレートエラー | Hamlレンダリング失敗 | エラーログ出力 |
| 宛先不正 | メールアドレス形式不正 | 送信スキップ |
| 新プロジェクトアクセス不可 | 受信者に権限なし | リンクなしメッセージ表示 |

### リトライ仕様

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

## 配信設定

### レート制限

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

### 配信時間帯

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

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

- 機密Issue（confidential: true）の場合、X-GitLab-ConfidentialIssueヘッダーがtrueに設定される
- 受信者が新しいプロジェクトへのアクセス権を持つかの確認を実施（@can_access_project）
- アクセス権がない場合は新しいIssueへのリンクを表示せず、「You don't have access to the project.」と表示
- recipient.can?(:read_project, @new_project)で権限チェック

## 備考

- 引数として受信者オブジェクト（User）が直接渡される（IDではない）
- new_issueオブジェクトが直接引数で渡される
- updated_by_userオブジェクトが直接引数で渡される
- @can_access_projectにより、受信者ごとにメール内容が異なる可能性がある
- issue_cloned_emailと類似の構造

---

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

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

### 推奨読解順序

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

まず、Issue移動関連のデータ構造を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | issue.rb | `app/models/issue.rb` | moved_to_id、moved_from関連（79-80行目） |

**読解のコツ**: Issueのmoved_to/moved_from関連を理解することで、移動前後のIssue参照が把握できる。

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

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | notification_service.rb | `app/services/notification_service.rb` | issue_movedメソッド（526-534行目） |

**主要処理フロー**:
1. **526行目**: `def issue_moved(issue, new_issue, current_user)` - 公開メソッド定義（new_issue引数あり）
2. **527行目**: BuildService.build_recipients呼び出し（action: 'moved'）
3. **529-533行目**: 各受信者にissue_moved_emailを送信（recipient.user, issue, new_issue, current_user）

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

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

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

**主要処理フロー**:
- **134行目**: `def issue_moved_email(recipient, issue, new_issue, updated_by_user, reason = nil)` - オブジェクト直接渡し
- **135行目**: setup_issue_mail(issue.id, recipient.id)でインスタンス変数設定
- **137行目**: @new_issue = new_issue
- **138行目**: @new_project = new_issue.project
- **139行目**: @can_access_project = recipient.can?(:read_project, @new_project) - 権限チェック
- **140-147行目**: mail_answer_thread実行

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

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

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

**主要処理フロー**:
- **2行目**: "Issue was moved to another project."メッセージ
- **3-5行目**: @can_access_projectがtrueの場合、新Issueへのリンク表示
- **6-7行目**: @can_access_projectがfalseの場合、「You don't have access」メッセージ

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

```
Issues::MoveService
    │
    └─ NotificationService#issue_moved (notification_service.rb:526)
           │
           ├─ NotificationRecipients::BuildService.build_recipients
           │      │
           │      └─ action: 'moved'
           │
           └─ recipients.map do |recipient|
                  │
                  └─ Notify.issue_moved_email(recipient.user, issue, new_issue, current_user, reason)
                         │
                         └─ Notify#issue_moved_email (emails/issues.rb:134)
                                │
                                ├─ #setup_issue_mail(issue.id, recipient.id)
                                │
                                ├─ @new_issue設定
                                ├─ @new_project設定
                                ├─ @can_access_project = recipient.can?(:read_project, @new_project)
                                │
                                └─ #mail_answer_thread
```

### データフロー図

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

Issue移動実行 ──────────────▶ Issues::MoveService ──────────▶ new_issue
     │                            │                              │
     │                            ▼                              │
     │                  NotificationService                      │
     │                  #issue_moved                             │
     │                            │                              │
     │                            ▼                              │
     │                     BuildService ────────────────────────▶ Sidekiqキュー
     │                     (受信者決定)                              │
     │                            │                              │
     └──────────────────▶ Emails::Issues ─────────────────────────┤
                          #issue_moved_email                     │
                                │                                │
                                ├─ @new_issue                    │
                                ├─ @new_project                  │
                                ├─ @can_access_project（受信者ごと）│
                                │                                │
                                ▼                                ▼
                        SentNotification ──────────▶ p_sent_notifications
                        (記録作成)                    テーブル
                                │
                                ▼
                        ActionMailer ────────────────▶ SMTPサーバー
                        (メール送信)                   └─▶ 関係者メールボックス
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| notification_service.rb | `app/services/notification_service.rb` | サービス | 通知処理のエントリーポイント（526-534行目） |
| issues.rb | `app/mailers/emails/issues.rb` | メーラー | issue_moved_emailメソッド定義（134-148行目） |
| notify.rb | `app/mailers/notify.rb` | メーラー | 共通メール処理・ヘッダー設定 |
| issue_moved_email.html.haml | `app/views/notify/issue_moved_email.html.haml` | テンプレート | HTMLメール本文（全8行） |
| issue.rb | `app/models/issue.rb` | モデル | Issueデータモデル（moved_to_id関連） |
