# 通知設計書 84-access_denied_email

## 概要

本ドキュメントは、GitLabにおける「アクセス拒否通知（access_denied_email）」の設計を定義するものである。プロジェクトまたはグループへのアクセスリクエストが拒否された際に、リクエストを行ったユーザーに通知メールを送信する。

### 本通知の処理概要

本通知は、ユーザーがプロジェクトまたはグループへのアクセスをリクエストし、そのリクエストが管理者によって拒否された際に、リクエスト者に対して拒否を通知するメールを送信する機能である。

**業務上の目的・背景**：アクセスリクエストの結果をリクエスト者に明確に伝えることで、透明性のあるアクセス管理を実現する。リクエスト者は自分のリクエストが処理されたことを認識でき、必要に応じて別のアクションを検討できる。また、プライベートなプロジェクト/グループの場合、名前を隠蔽することでセキュリティを確保する。

**通知の送信タイミング**：アクセスリクエストが管理者によって拒否され、Memberレコードが削除された直後に送信される。`Members::DestroyService` または関連サービスでリクエストが拒否された際にトリガーされる。

**通知の受信者**：アクセスリクエストを行ったユーザー本人。通知設定で `:subscription` レベルが無効化されている場合は送信されない。

**通知内容の概要**：アクセスが拒否されたプロジェクト/グループの名前（閲覧権限がない場合は「Hidden」）、およびプロジェクト/グループへのリンク（閲覧権限がある場合のみ）が含まれる。

**期待されるアクション**：受信者は通知を確認し、アクセスが必要な場合は管理者に直接連絡するか、別の方法でアクセスを取得する手段を検討する。

## 通知種別

メール（テキスト形式）

## 送信仕様

### 基本情報

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

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

1. Memberレコードからuserを取得
2. `user.notification_email_for(member_source.notification_group)` でメールアドレスを取得
3. `member.notifiable?(:subscription)` で通知可否を判定

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | Gitlab.config.gitlab.email_from |
| 送信元名称 | Gitlab.config.gitlab.email_display_name |
| 件名 | `Access to the {source_name} {source_type} was denied` |
| 形式 | テキスト形式 |

### 本文テンプレート

```
Your request to join the {source_name} {source_type} has been denied.

{source_url}
```

※ `source_name` はソースが非表示の場合は「Hidden」
※ `source_url` はソースが非表示の場合は省略

### 添付ファイル

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

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| source_name | プロジェクト/グループ名（またはHidden） | member_source.human_name または 'Hidden' | Yes |
| source_type | ソースタイプ（project/group） | member_source.model_name.singular | Yes |
| source_url | プロジェクト/グループURL | member_source.web_url | No（source_hidden?の場合は省略） |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 画面操作 | アクセスリクエスト拒否 | notifiable?(:subscription) | メンバー管理画面からリクエストを拒否 |
| API | Member削除（リクエスト状態） | notifiable?(:subscription) | API経由でのリクエスト拒否 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| notifiable?(:subscription) = false | 通知設定で無効化されている |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[アクセスリクエスト拒否] --> B{notifiable?:subscription}
    B -->|No| C[送信スキップ]
    B -->|Yes| D[AccessDeniedMailer.email]
    D --> E[mail_with_locale]
    E --> F[deliver_later]
    F --> G[Sidekiqジョブキュー]
    G --> H[メール送信完了]
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| members | メンバー/リクエスト情報取得 | user_id, source_id, source_type |
| users | ユーザー情報取得 | 送信先メールアドレス |
| projects | プロジェクト情報 | source_type='Project'の場合 |
| namespaces | グループ/名前空間情報 | source_type='Namespace'の場合 |
| notification_settings | 通知設定確認 | notifiable?判定用 |

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

#### members

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | メンバー識別 | paramsで渡される |
| user_id | ユーザーID | - |
| source_id | ソースID | - |
| source_type | ソースタイプ | - |

#### users

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | ユーザー識別 | members.user_id |
| email | メールアドレス | 送信先 |
| notification_email | 通知用メール | 設定されている場合 |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| なし | - | 本通知では更新処理なし |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| SMTP接続エラー | メールサーバー接続失敗 | Sidekiqリトライ |

### リトライ仕様

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

## 配信設定

### レート制限

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

### 配信時間帯

特に制限なし（リクエスト拒否時に即座に送信）

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

- プライベートなプロジェクト/グループへのアクセスが拒否された場合、ソース名は「Hidden」として表示される
- `source_hidden?` は `member_source.readable_by?(user)` で判定される
- ソースが非表示の場合、URLもメール本文に含まれない

## 備考

- 件名でもソース名が「Hidden」として表示される可能性がある
- レイアウトは標準の `mailer` レイアウトを使用
- `params[:member]` でMemberオブジェクトが渡される

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | member.rb | `app/models/member.rb` | request?メソッド（536-538行目）を確認 |

**読解のコツ**: `request?` は `requested_at.present?` を返す。リクエスト状態のメンバーは `requested_at` が設定されている。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | access_denied_mailer.rb | `app/mailers/members/access_denied_mailer.rb` | emailメソッド（11-18行目）を確認 |

**主要処理フロー**:
- **12行目**: notifiable?(:subscription)チェック
- **14-17行目**: mail_with_locale呼び出し

#### Step 3: プライバシー保護ロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | access_denied_mailer.rb | `app/mailers/members/access_denied_mailer.rb` | source_hidden?（25-27行目）を確認 |

**主要処理フロー**:
- **25-27行目**: source_hidden?の実装 - `!member_source.readable_by?(user)`
- **33-36行目**: email_subject_text - source_hidden?に基づく件名生成

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

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

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

```
Members::DestroyService（リクエスト拒否時）
    │
    └─ Members::AccessDeniedMailer.with(member: member).email
           │
           ├─ notifiable?(:subscription) チェック
           │
           ├─ source_hidden? 判定
           │      └─ member_source.readable_by?(user)
           │
           └─ mail_with_locale → deliver_later
```

### データフロー図

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

リクエスト拒否    ───▶ AccessDeniedMailer.email    ───▶ メール送信（Sidekiq）
    │                      │
    ├─ member              ├─▶ source_hidden? 判定
    └─ user                │      │
                           │      └─▶ readable_by? チェック
                           │
                           └─▶ email_subject_text
                                  │
                                  └─▶ "Hidden" or actual name
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| access_denied_mailer.rb | `app/mailers/members/access_denied_mailer.rb` | ソース | アクセス拒否メーラー |
| email.text.erb | `app/views/members/access_denied_mailer/email.text.erb` | テンプレート | テキスト形式メールテンプレート |
| member.rb | `app/models/member.rb` | ソース | Memberモデル |
| destroy_service.rb | `app/services/members/destroy_service.rb` | ソース | メンバー削除サービス（呼び出し元） |
