# 通知設計書 11-フォーラム投稿通知（トピック購読者向け）

## 概要

本ドキュメントは、QuickerSite CMSにおける「フォーラム投稿通知（トピック購読者向け）」機能の通知設計について記述する。特定のフォーラムトピックを購読しているユーザーに対して、返信があった際にメール通知を自動送信する機能である。

### 本通知の処理概要

**業務上の目的・背景**：フォーラム機能において、ユーザーが関心のあるトピックに対する新規返信を見逃さないようにすることが目的である。トピック単位で購読を設定することで、テーマ全体を購読するより詳細な通知管理が可能となり、ユーザーは自分が興味を持つ議論の進展を迅速に把握できる。これによりコミュニティの活性化とユーザーエンゲージメントの向上を実現する。

**通知の送信タイミング**：フォーラムトピックに対して新規返信（コメント）が投稿され、`post.Save`関数が正常に完了した直後に送信される。投稿者本人への重複送信は除外される。

**通知の受信者**：`tblThemeTopicSubscription`テーブルに登録され、かつ有効なメールアドレスを持つコンタクト（投稿者本人を除く）が対象となる。購読解除済みのユーザーや、テーマ全体を購読しているユーザーは除外される。

**通知内容の概要**：返信者名（ニックネーム）、元のトピックの件名、返信内容本文、添付ファイルリンク（存在する場合）、トピックへの直接リンクが含まれる。

**期待されるアクション**：受信者は通知メールを確認し、興味がある場合はリンクをクリックしてフォーラムにアクセスし、議論に参加することが期待される。

## 通知種別

メール

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 同期 |
| 優先度 | 中 |
| リトライ | 無（`On Error Resume Next`によりエラーは無視される） |

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

1. `tblThemeTopicSubscription`テーブルから対象トピック（`iPostID`）を購読しているコンタクトIDを取得
2. `tblContact`テーブルと結合し、有効なメールアドレス（`sEmail`が空でない、かつNULLでない）を持つコンタクトを抽出
3. 投稿者本人（`logon.contact.iId`と一致するコンタクト）を除外
4. 残ったコンタクト全員にメールを送信

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | `customer.webmasterEmail`（顧客設定のウェブマスターメール） |
| 送信元名称 | `customer.siteName`（サイト名） |
| 件名 | `theme.sSubjectNotification`（テーマ設定の通知件名） |
| 形式 | HTML |

### 本文テンプレート

デフォルトテンプレート（`cls_theme`クラスで定義）:

```html
<p>You are receiving this email because you requested to be notified when somebody responded to the topic <em>[QS_theme:postsubject]</em>.</p>
<p><strong>[QS_theme:Replyer]</strong> has replied.</p>
<p><i>[QS_theme:reply]</i></p>
<p>Click the link below or paste it into your browser to read the reply online.</p>
<p><a href='[QS_theme:postlink]'>[QS_theme:postlink]</a></p>
```

### 添付ファイル

| ファイル名 | 形式 | 条件 | 説明 |
|----------|------|------|------|
| なし | - | - | 添付ファイルはメール本文内のリンクとして表示される |

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| `[QS_theme:postsubject]` | 元トピックの件名 | `parentTopic.sSubject` | Yes |
| `[QS_theme:Replyer]` | 返信者名 | `sAnName`または`logon.contact.sNickname` | Yes |
| `[QS_theme:reply]` | 返信内容（HTMLまたはテキスト） | `sBody` + `sFileLink()` | Yes |
| `[QS_theme:postlink]` | トピックへのリンクURL | `prepareLink()`関数 | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 画面操作 | 返信投稿（`cls_post.Save`関数） | 新規投稿かつ`iPostID<>0`（返信） | トピックへの返信が保存された時 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| `theme.iSubLevel <= QS_theme_sublevel_none` | テーマがトピック購読をサポートしていない場合 |
| `iContactID = logon.contact.iId` | 購読者が投稿者本人の場合 |
| `sEmail`が空またはNULL | メールアドレスが登録されていない場合 |
| 更新（非新規）投稿 | `isNew = false`の場合は通知しない |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[返信投稿] --> B{新規投稿か?}
    B -->|No| Z[終了]
    B -->|Yes| C{iPostID<>0?}
    C -->|No| Z
    C -->|Yes| D{iSubLevel>none?}
    D -->|No| Z
    D -->|Yes| E[トピック購読者を取得]
    E --> F[SQLで購読者リスト取得]
    F --> G{購読者あり?}
    G -->|No| Z
    G -->|Yes| H[ループ開始]
    H --> I{投稿者本人?}
    I -->|Yes| J[スキップ]
    I -->|No| K[メール作成]
    K --> L[テンプレート展開]
    L --> M[メール送信]
    M --> N{次の購読者?}
    N -->|Yes| H
    N -->|No| Z
    J --> N
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| tblThemeTopicSubscription | トピック購読者の取得 | 購読関係の管理 |
| tblContact | 購読者のメールアドレス取得 | ユーザー情報 |
| tblPost | 投稿情報の取得 | 返信内容と親トピック |
| tblTheme | テーマ設定の取得 | 通知テンプレート設定 |

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

#### tblThemeTopicSubscription

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| iContactID | 購読者のコンタクトID | `WHERE iPostID = {対象トピックID}` |

#### tblContact

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| sEmail | 通知送信先メールアドレス | `WHERE sEmail <> '' AND sEmail IS NOT NULL` |
| sNickName | 購読者名（参考） | INNER JOIN |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| なし | - | 通知送信処理ではDBを更新しない |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 送信失敗 | SMTPサーバー接続エラー | `On Error Resume Next`により無視して継続 |
| テンプレートエラー | 変数置換失敗 | 処理継続（部分的な表示） |
| 宛先不正 | メールアドレス形式不正 | SQLの段階でフィルタリング |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 0（リトライなし） |
| リトライ間隔 | - |
| リトライ対象エラー | - |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 制限なし |
| 1日あたり上限 | 制限なし |

### 配信時間帯

配信時間帯の制限なし。投稿が行われた時点で即座に送信される。

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

- メール本文には`quotrep()`関数によりHTMLエスケープされたコンテンツが含まれる
- 投稿者本人への重複送信は`iContactID<>logon.contact.iId`条件で防止
- XSS対策として`addSmilies()`関数経由でのサニタイズ処理が実施される

## 備考

- テーマ設定で`iSubLevel`が`QS_theme_sublevel_topic`以上の場合のみ、トピック購読機能が有効
- テーマ全体を購読しているユーザーは、トピック購読通知ではなくテーマ購読通知を受け取る
- 通知メールには投稿者のアバター画像も含まれる（`customer.bUseAvatars`が有効な場合）

---

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

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

### 推奨読解順序

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

トピック購読の管理テーブルと関連エンティティの構造を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | theme.asp | `asp/includes/theme.asp` | cls_themeクラスのプロパティ定義（iSubLevel, sBodyNotification等） |
| 1-2 | post.asp | `asp/includes/post.asp` | cls_postクラスのプロパティ定義（iPostID, iThemeID, sSubject等） |

**読解のコツ**: Classic ASPのクラス定義では`Public`宣言されたプロパティがフィールドに相当する。

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

返信投稿の保存処理が通知のトリガーとなる。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | post.asp | `asp/includes/post.asp` | cls_post.Save関数が通知処理のエントリーポイント |

**主要処理フロー**:
1. **行107-156**: Save関数の冒頭でチェック処理、レコード保存
2. **行170**: `if isNew then` - 新規投稿の場合のみ通知処理へ
3. **行198-217**: トピック購読者向け通知の送信ループ

#### Step 3: 購読者取得処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | post.asp | `asp/includes/post.asp` | 行199-217のSQLとループ処理 |

**主要処理フロー**:
- **行199**: `if convertGetal(iPostID)<>0 then` - 返信であることの確認
- **行201-205**: トピック購読者取得SQL構築
- **行206-216**: 購読者ループで個別メール送信

#### Step 4: メール送信処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | mail_message.asp | `asp/includes/mail_message.asp` | cls_mail_messageクラスのsend関数 |
| 4-2 | post.asp | `asp/includes/post.asp` | preparenotifyReply関数（行729-748） |

**主要処理フロー**:
- **行208-214**: メールオブジェクト作成、件名・本文設定、送信
- **行729-748**: テンプレート変数の置換処理

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

```
default.asp
    │
    └─ cls_theme.build()
           │
           └─ cls_post.buildPost() [返信フォーム表示・処理]
                  │
                  └─ cls_post.Save() [行107]
                         │
                         ├─ Check() [入力検証]
                         │
                         ├─ [DBへの保存処理]
                         │
                         └─ [通知処理 - 行170以降]
                                │
                                ├─ [テーマ購読者向け通知 - 行175-197]
                                │
                                └─ [トピック購読者向け通知 - 行198-217]
                                       │
                                       ├─ SQLで購読者取得
                                       │
                                       └─ cls_mail_message.send()
                                              │
                                              └─ preparenotifyReply() [テンプレート展開]
```

### データフロー図

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

返信フォーム ───────▶ cls_post.Save() ───────▶ tblPostへINSERT
    │                      │
    │                      ▼
    │              tblThemeTopicSubscription
    │              から購読者取得
    │                      │
    │                      ▼
    │              preparenotifyReply()
    │              [テンプレート展開]
    │                      │
    │                      ▼
    └─────────────▶ cls_mail_message.send() ───▶ メール送信
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| post.asp | `asp/includes/post.asp` | ソース | 投稿クラス定義、通知送信の主要ロジック |
| theme.asp | `asp/includes/theme.asp` | ソース | テーマクラス定義、購読レベル管理 |
| mail_message.asp | `asp/includes/mail_message.asp` | ソース | メール送信クラス定義 |
| contact.asp | `asp/includes/contact.asp` | ソース | コンタクトクラス定義、購読状態確認 |
| functions.asp | `asp/includes/functions.asp` | ソース | ユーティリティ関数（quotrep, addSmilies等） |
