# 通知設計書 5-closed_issue_email

## 概要

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

### 本通知の処理概要

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

**業務上の目的・背景**：プロジェクト管理において、タスクの完了（クローズ）は重要なマイルストーンである。Issue作成者、担当者、関係者がIssueのクローズを認識することで、作業の完了確認、後続タスクの開始判断、プロジェクト進捗の把握が可能になる。この通知により、チーム全体が作業状況を把握し、適切なフォローアップを行える。

**通知の送信タイミング**：IssueのステータスがOpenからClosedに変更された直後に、非同期（Sidekiq経由）でメール送信がトリガーされる。NotificationService.new.close_issue(issue, current_user, params)が呼び出されることで処理が開始される。

**通知の受信者**：Issue作成者、Issue担当者、通知レベルがParticipating以上のプロジェクトメンバー、カスタム通知設定で「close issue」を有効にしているユーザーが受信対象となる。クローズを実行したユーザーは通知対象から除外される。

**通知内容の概要**：クローズを実行したユーザー名、クローズの理由（Merge Requestによるクローズ、直接クローズなど）が含まれる。

**期待されるアクション**：受信者はIssueのクローズを確認し、必要に応じて再オープン、関連Issue作成、完了報告などのアクションを行うことが期待される。

## 通知種別

メール通知

## 送信仕様

### 基本情報

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

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

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

1. action: "close"で受信者を構築
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 closed by {user_name}. （直接クローズの場合）
Issue was closed by {user_name} via merge request {mr_reference}. （MRによるクローズの場合）
Issue was closed by {user_name} via commit {commit_sha}. （コミットによるクローズの場合）

Issue #{iid}: {issue_url}
```

### 添付ファイル

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

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| @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 |
| @updated_by | クローズ実行者 | User.find(updated_by_user_id) | Yes |
| @closed_via | クローズ理由オブジェクト | MergeRequest/Commit等 | No |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 画面操作 | Issue詳細画面でクローズボタン押下 | state: 'closed' | WebUIからの直接クローズ |
| API | PUT /api/v4/projects/:id/issues/:issue_iid | state_event: 'close' | REST API経由のクローズ |
| クイックアクション | /close コマンド | コマンド実行成功 | コメント経由のクローズ |
| Merge Request | MRマージ時 | Closes #xxx参照 | MRマージによる自動クローズ |
| コミット | プッシュ時 | Fixes #xxx参照 | コミットプッシュによる自動クローズ |

### 送信抑止条件

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

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[Issueクローズ] --> B[NotificationService.close_issue]
    B --> C[close_resource_email呼び出し]
    C --> D[action: close]
    D --> E[NotificationRecipients::BuildService.build_recipients]
    E --> F[受信者リスト構築]
    F --> G[各受信者に対してループ]
    G --> H[Notify.closed_issue_email.deliver_later]
    H --> I[Sidekiqキューに追加]
    I --> J[setup_issue_mail実行]
    J --> K[@closed_via設定]
    K --> L[@updated_by設定]
    L --> M[mail_answer_thread実行]
    M --> N[メール送信]
    N --> O[終了]
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| issues | Issue情報取得 | メール本文データ、state確認 |
| users | ユーザー情報取得 | 作成者・受信者・クローズ実行者 |
| projects | プロジェクト情報 | メール件名・ヘッダー |
| namespaces | 名前空間情報 | グループ情報取得 |
| notification_settings | 通知設定 | 受信者フィルタリング |
| merge_requests | MR情報 | closed_viaがMRの場合 |

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

#### issues

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

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| 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クラスで実施

## 備考

- closed_viaパラメータにより、クローズの原因となったオブジェクト（MergeRequest, Commit等）を特定可能
- closure_reason_textヘルパーがクローズ理由のメッセージを生成
- Work Itemsタイプのissueでも同様の通知が送信される（work_item_type_for使用）
- テキスト形式メールでは@target_urlが直接表示される

---

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

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

### 推奨読解順序

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

まず、Issue状態とクローズ関連のデータ構造を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | issue.rb | `app/models/issue.rb` | state関連、closed_by関連 |
| 1-2 | state_eventable.rb | `app/models/concerns/state_eventable.rb` | 状態遷移の定義 |

**読解のコツ**: Issueのstate（open/closed）と状態遷移イベントを理解することで、クローズ処理の流れが把握できる。

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

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | notification_service.rb | `app/services/notification_service.rb` | close_issueメソッド（233-234行目）、close_resource_email（832-845行目） |

**主要処理フロー**:
1. **233行目**: `def close_issue(issue, current_user, params = {})` - 公開メソッド定義
2. **234行目**: `close_resource_email(issue, current_user, :closed_issue_email, closed_via: params[:closed_via])` 呼び出し
3. **832-845行目**: close_resource_emailでaction: "close"、受信者構築・メール送信

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

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

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

**主要処理フロー**:
- **56行目**: `def closed_issue_email(recipient_id, issue_id, updated_by_user_id, reason: nil, closed_via: nil)` - キーワード引数
- **57行目**: setup_issue_mailに closed_via オプション渡し
- **59行目**: @updated_by = User.find(updated_by_user_id)

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

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

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

**主要処理フロー**:
- **2行目（HTML）**: `closure_reason_text(@closed_via, format: formats.first, name: @updated_by.name)`
- closure_reason_textヘルパーがクローズ理由に応じたメッセージを生成

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

```
Issues::CloseService / MergeRequests::PostMergeService
    │
    └─ NotificationService#close_issue (notification_service.rb:233)
           │
           └─ #close_resource_email (notification_service.rb:832)
                  │
                  ├─ action = "close"
                  │
                  ├─ NotificationRecipients::BuildService.build_recipients
                  │      │
                  │      └─ Builder::Default#build!
                  │             ├─ add_participants
                  │             ├─ add_watchers
                  │             └─ add_subscribed_users
                  │
                  └─ recipients.each { Notify.closed_issue_email.deliver_later }
                         │
                         └─ Notify#closed_issue_email (emails/issues.rb:56)
                                │
                                ├─ #setup_issue_mail(closed_via: closed_via)
                                │
                                ├─ @updated_by = User.find
                                │
                                └─ #mail_answer_thread
```

### データフロー図

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

Issueクローズイベント ──────▶ Issues::CloseService ──────────▶ closed_via
     │                            │                              │
     │                            ▼                              │
     │                  NotificationService                      │
     │                  #close_issue                             │
     │                            │                              │
     │                            ▼                              │
     │                  #close_resource_email ──────────────────▶ Sidekiqキュー
     │                            │                              │
     │                            ▼                              │
     └──────────────────▶ Emails::Issues ─────────────────────────┤
                          #closed_issue_email                    │
                                │                                │
                                ├─ @closed_via                   │
                                ├─ @updated_by                   │
                                │                                │
                                ▼                                ▼
                        SentNotification ──────────▶ p_sent_notifications
                        (記録作成)                    テーブル
                                │
                                ▼
                        ActionMailer ────────────────▶ SMTPサーバー
                        (メール送信)                   └─▶ 関係者メールボックス
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| notification_service.rb | `app/services/notification_service.rb` | サービス | 通知処理のエントリーポイント（233-234行目、832-845行目） |
| issues.rb | `app/mailers/emails/issues.rb` | メーラー | closed_issue_emailメソッド定義（56-69行目） |
| notify.rb | `app/mailers/notify.rb` | メーラー | 共通メール処理・ヘッダー設定 |
| closed_issue_email.html.haml | `app/views/notify/closed_issue_email.html.haml` | テンプレート | HTML形式メール本文 |
| closed_issue_email.text.haml | `app/views/notify/closed_issue_email.text.haml` | テンプレート | テキスト形式メール本文 |
| default.rb | `app/services/notification_recipients/builder/default.rb` | サービス | 受信者構築ロジック |
| issue.rb | `app/models/issue.rb` | モデル | Issueデータモデル |
| emails_helper.rb | `app/helpers/emails_helper.rb` | ヘルパー | closure_reason_text関数 |
