# 通知設計書 11-issue_cloned_email

## 概要

本ドキュメントは、GitLabにおけるIssueが複製（クローン）された際に送信されるメール通知「issue_cloned_email」の設計仕様を定義するものである。

### 本通知の処理概要

この通知は、Issue（課題）が別のプロジェクトまたは同一プロジェクト内で複製された際に、関係者に対して複製の完了を通知するメールを送信する機能である。

**業務上の目的・背景**：プロジェクト管理において、既存のIssueを別のプロジェクトやコンテキストで再利用したい場合がある。Issueの複製機能により、同様の内容を持つ新しいIssueを効率的に作成できる。この通知により、元のIssueに関心を持つユーザー（作成者、担当者、参加者等）が複製の事実を把握し、必要に応じて新しいIssueの追跡や確認を行うことができる。

**通知の送信タイミング**：Issueが複製操作（clone）により新しいIssueとして作成された直後に送信される。具体的には、`NotificationService#issue_cloned`メソッドが呼び出された時点で、対象となる受信者に対して非同期でメールが配信される。

**通知の受信者**：元のIssueに関連する関係者が受信者となる。具体的には、NotificationRecipients::BuildServiceによって'cloned'アクションに基づき決定され、Issueの作成者、担当者、購読者、およびプロジェクトのWatch設定をしているメンバーが含まれる。

**通知内容の概要**：メールには、誰がIssueを複製したか、元のIssueへの参照リンク、および新しいIssue（複製先）への参照リンクが含まれる。受信者が新しいプロジェクトへのアクセス権限を持たない場合は、その旨が通知される。

**期待されるアクション**：受信者は通知を確認し、必要に応じて新しく作成されたIssueを確認・追跡する。新しいプロジェクトへのアクセス権がない場合は、適切なアクセス権限のリクエストを検討する。

## 通知種別

メール通知

## 送信仕様

### 基本情報

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

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

受信者は`NotificationRecipients::BuildService.build_recipients`メソッドにより決定される。action: 'cloned'パラメータが渡され、以下の条件に基づいて受信者リストが構築される：

1. Issueの作成者（通知設定がDisabled以外の場合）
2. Issueの担当者（assignees）
3. プロジェクトのWatch設定をしているユーザー
4. Issueを購読しているユーザー
5. カスタム通知レベルで該当アクションを有効にしているユーザー

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | GitLabインスタンスのデフォルト送信元アドレス |
| 送信元名称 | "{操作実行者の名前} (@{ユーザー名})" |
| 件名 | "Re: {プロジェクト名} \| {Issue タイトル} (#{Issue IID})" |
| 形式 | HTML/テキスト（マルチパート） |

### 本文テンプレート

**HTMLバージョン**
```
{author_link} cloned {original_issue} to {new_issue}.
```
または（新しいプロジェクトへのアクセス権がない場合）：
```
{author_link} cloned {original_issue}. You don't have access to the new project.
```

**テキストバージョン**
```
Issue was cloned.

New issue location:
{新しいIssueのURL}
```
または（アクセス権がない場合）：
```
Issue was cloned.

You don't have access to the project.
```

### 添付ファイル

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

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| @author | 複製を実行したユーザー | User.find(updated_by_user_id) | Yes |
| @issue | 元のIssue | Issue.find(issue_id) | Yes |
| @new_issue | 複製先の新しいIssue | 引数で渡される | Yes |
| @can_access_project | 受信者が新しいプロジェクトへのアクセス権を持つか | recipient.can?(:read_project, @new_issue.project) | Yes |
| @recipient | メール受信者 | User.find(recipient_id) | Yes |
| @target_url | 元のIssueへのURL | Gitlab::UrlBuilder.build(@issue) | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 画面操作/API | Issue複製アクション | Issueの複製が成功した場合 | ユーザーがUIまたはAPIを通じてIssueを複製した際に発火 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| 通知設定が無効 | ユーザーの通知設定がDisabledの場合は送信されない |
| プロジェクトのメール無効設定 | プロジェクトでメール通知が無効化されている場合 |
| 操作実行者が通知をトリガーできない | can_trigger_notifications?がfalseの場合 |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[Issue複製操作実行] --> B[NotificationService.issue_cloned呼び出し]
    B --> C[BuildService.build_recipients実行]
    C --> D{受信者あり?}
    D -->|No| E[処理終了]
    D -->|Yes| F[各受信者に対してループ]
    F --> G[Notify.issue_cloned_email生成]
    G --> H[deliver_later実行]
    H --> I[Sidekiqキューに追加]
    I --> J[非同期でメール送信]
    J --> K[SentNotification記録]
    K --> L[処理完了]
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| issues | 元のIssueと新しいIssueの情報取得 | |
| users | 受信者、操作実行者の情報取得 | |
| projects | プロジェクト情報の取得 | |
| namespaces | 名前空間情報の取得 | |
| notification_settings | ユーザーの通知設定確認 | |
| members | プロジェクト/グループメンバーシップ確認 | |

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

#### issues

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | Issue識別子 | 引数で指定 |
| title | メール件名に使用 | |
| iid | Issue番号（プロジェクト内） | |
| author_id | Issue作成者ID | |
| project_id | 所属プロジェクトID | |
| confidential | 機密Issue判定 | |

#### users

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | ユーザー識別子 | |
| name | 表示名 | |
| email | 通知先メールアドレス | |
| notification_email | 通知用メールアドレス | |

### 更新テーブル一覧

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

#### sent_notifications（p_sent_notifications）

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|-------|------|
| INSERT | recipient_id | 受信者のユーザーID | |
| INSERT | noteable_type | 'Issue' | |
| INSERT | noteable_id | IssueのID | |
| INSERT | project_id | プロジェクトID | |
| INSERT | reply_key | ランダム生成キー | メール返信用 |
| INSERT | namespace_id | 名前空間ID | シャーディングキー |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 送信失敗 | SMTPサーバー接続失敗 | Sidekiqによる自動リトライ |
| テンプレートエラー | テンプレート変数の欠落 | エラーログ記録、管理者通知 |
| 宛先不正 | メールアドレス形式不正 | 該当受信者をスキップしてログ記録 |
| Issueが見つからない | Issue.find失敗 | ActiveRecord::RecordNotFoundが発生 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | Sidekiqデフォルト（25回） |
| リトライ間隔 | 指数バックオフ |
| リトライ対象エラー | 一時的なネットワークエラー、SMTP接続エラー |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | Gitlab::ApplicationRateLimiterの:notification_emails設定に従う |
| 1日あたり上限 | 設定による |

### 配信時間帯

特定の時間帯制限なし（即時配信）

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

- 機密Issue（confidential: true）の場合、X-GitLab-ConfidentialIssueヘッダーに'true'が設定される
- 受信者が新しいプロジェクトへのアクセス権を持たない場合、新しいIssueの詳細URLは表示されない
- メール返信機能使用時はreply_keyによる認証が行われる
- SentNotificationに記録されることで、返信時の認証と購読解除が可能

## 備考

- この通知はスレッド形式のメールとして送信され、同一Issueに関する通知はメールクライアントでグループ化される
- 購読解除リンクがメールに含まれ、ユーザーは個別に通知を停止できる

---

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

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

### 推奨読解順序

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

まず、通知に関連するデータモデルを理解することが重要である。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | sent_notification.rb | `app/models/sent_notification.rb` | 送信通知の記録モデル。reply_keyの生成、partitioned_reply_keyの仕組みを理解する |
| 1-2 | notification_setting.rb | `app/models/notification_setting.rb` | ユーザーの通知設定モデル |

**読解のコツ**: SentNotificationはパーティションテーブルを使用しており、partition_idとreply_keyの組み合わせで一意性を保証している。

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

処理の起点となるNotificationServiceを特定する。

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

**主要処理フロー**:
1. **536行目**: `def issue_cloned(issue, new_issue, current_user)` - メソッド定義
2. **537行目**: `NotificationRecipients::BuildService.build_recipients` - 受信者リスト構築
3. **539-543行目**: 各受信者に対してメール送信をループ

#### Step 3: メーラーモジュールを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | issues.rb | `app/mailers/emails/issues.rb` | issue_cloned_emailメソッド（150-165行目）でメール生成処理を確認 |
| 3-2 | notify.rb | `app/mailers/notify.rb` | 基底クラス。mail_answer_thread、sender、subjectメソッドの動作を理解 |

**主要処理フロー**:
- **150行目**: `def issue_cloned_email(recipient, issue, new_issue, updated_by_user, reason = nil)`
- **151行目**: `setup_issue_mail` - 共通のIssueメール設定
- **153-156行目**: インスタンス変数の設定（@author, @issue, @new_issue, @can_access_project）
- **157-164行目**: `mail_answer_thread`でメールスレッドとして送信

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | issue_cloned_email.html.haml | `app/views/notify/issue_cloned_email.html.haml` | HTML形式のメール本文テンプレート |
| 4-2 | issue_cloned_email.text.erb | `app/views/notify/issue_cloned_email.text.erb` | プレーンテキスト形式のメール本文 |

**主要処理フロー**:
- **1行目**: author_linkの生成
- **2-5行目**: アクセス権限による条件分岐
- **6-7行目**: メッセージの組み立てと出力

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

```
WorkItems::DataSync::CloneService#execute
    |
    └── NotificationService#issue_cloned
            |
            ├── NotificationRecipients::BuildService.build_recipients
            |       └── (action: 'cloned')
            |
            └── Notify.issue_cloned_email (for each recipient)
                    |
                    ├── setup_issue_mail
                    |       ├── Issue.find
                    |       ├── User.find (recipient)
                    |       └── SentNotification.record
                    |
                    ├── mail_answer_thread
                    |       ├── add_project_headers
                    |       ├── add_unsubscription_headers_and_links
                    |       └── mail_with_locale
                    |
                    └── deliver_later (Sidekiq)
```

### データフロー図

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

Issue (元)        ──┐
                    ├──▶ NotificationService     ──▶ Sidekiq Queue
Issue (新)        ──┤           │
                    │           ▼
User (実行者)     ──┤    BuildService           ──▶ recipient list
                    │           │
                    │           ▼
                    └──▶ Notify.issue_cloned_email
                                │
                                ├──▶ SentNotification (DB)
                                │
                                └──▶ ActionMailer::MessageDelivery
                                            │
                                            ▼
                                      SMTPサーバー ──▶ 受信者メールボックス
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| notification_service.rb | `app/services/notification_service.rb` | ソース | 通知サービスのエントリーポイント |
| issues.rb | `app/mailers/emails/issues.rb` | ソース | Issue関連メール生成モジュール |
| notify.rb | `app/mailers/notify.rb` | ソース | メーラー基底クラス |
| issue_cloned_email.html.haml | `app/views/notify/issue_cloned_email.html.haml` | テンプレート | HTML形式メールテンプレート |
| issue_cloned_email.text.erb | `app/views/notify/issue_cloned_email.text.erb` | テンプレート | テキスト形式メールテンプレート |
| sent_notification.rb | `app/models/sent_notification.rb` | ソース | 送信通知記録モデル |
| clone_service.rb | `app/services/work_items/data_sync/clone_service.rb` | ソース | Issue複製サービス |
| application_mailer.rb | `app/mailers/application_mailer.rb` | ソース | メーラー基底クラス |
