# SmsForwarder Sender Architecture: How to Add New Forwarding Targets

> Explore SmsForwarder's pluggable sender architecture. Learn how to add new forwarding targets by creating four components, extending its functionality without modifying the core.

- Repository: [pppscn/SmsForwarder](https://github.com/pppscn/SmsForwarder)
- Tags: architecture
- Published: 2026-06-22

---

**SmsForwarder implements a pluggable sender architecture that enables developers to add custom forwarding targets by creating four components—a type constant, a settings data class, a utility handler, and a UI fragment—without touching the core rule engine or background workers.**

The open-source SmsForwarder project (pppscn/SmsForwarder) processes incoming SMS and notification messages through a modular pipeline that decouples message reception from delivery logic. Understanding this sender architecture is essential for extending the app to support proprietary webhooks, internal APIs, or messaging platforms not included in the default distribution.

## Core Components of the Sender System

The architecture revolves around five distinct layers that handle persistence, configuration, execution, and routing.

### Sender Entity

The **Sender entity** represents a persistent configuration record stored in the Room database. Defined in [`app/src/main/kotlin/cn/ppps/forwarder/database/entity/Sender.kt`](https://github.com/pppscn/SmsForwarder/blob/main/app/src/main/kotlin/cn/ppps/forwarder/database/entity/Sender.kt), this entity contains an `id`, `type` integer, `status` flag, and a `jsonSetting` field that stores serialized configuration data. Rules reference lists of these sender objects to determine where messages should be routed.

### Setting Data Classes

Each sender type requires a **setting data class**—a plain Kotlin data class that models the specific configuration required for that transport mechanism. These classes reside in `app/src/main/kotlin/cn/ppps/forwarder/entity/setting/<SenderName>Setting.kt` and typically include fields like webhook URLs, API tokens, or SMTP credentials. Gson deserializes the `jsonSetting` string into these typed objects at runtime.

### Sender Utility Classes

The actual network operations reside in **sender utils**—static helper classes located in `app/src/main/kotlin/cn/ppps/forwarder/utils/sender/<SenderName>Utils.kt`. Each utility class implements a `sendMsg()` method that accepts the deserialized setting object, constructs the HTTP request or SDK call, and handles the response. These utilities are completely stateless and rely on the project’s XHttp library for network operations.

### Routing and Dispatch Logic

`SendUtils.sendMsgSender()` in [`app/src/main/kotlin/cn/ppps/forwarder/utils/SendUtils.kt`](https://github.com/pppscn/SmsForwarder/blob/main/app/src/main/kotlin/cn/ppps/forwarder/utils/SendUtils.kt) serves as the central router. This method extracts the sender from `rule.senderList[senderIndex]`, deserializes the JSON configuration into the appropriate setting class using `Gson().fromJson()`, and dispatches to the corresponding utility class via a type-checking `when` block.

### Rule Logic and Chaining

The **senderLogic** mechanism controls how multiple senders in a rule interact. Defined in `SendUtils.senderLogic()` (lines 21-27), the logic supports four modes: `ALL` (execute all senders), `UNTIL_SUCCESS` (stop after first success), `UNTIL_FAIL` (stop after first failure), and `RETRY`. After each sender completes, `SendLogicWorker` enqueues the next sender according to this logic, creating a resilient forwarding chain.

### Background Workers

Two WorkManager classes handle asynchronous execution: `SendWorker` handles single-send retries, while `SendLogicWorker` manages the sender chaining logic. Both reside in `app/src/main/kotlin/cn/ppps/forwarder/workers/` and operate on background threads to prevent blocking the main UI during network operations.

## Message Flow Through the Architecture

When a new SMS arrives, the system processes it through a standardized pipeline:

1. **Rule Matching**: `SendLogicWorker` builds a `MsgInfo` object and calls `SendUtils.sendMsgSender(msgInfo, rule, senderIndex, logId, msgId)`.
2. **Sender Selection**: The router extracts the sender at `rule.senderList[senderIndex]`.
3. **Configuration Deserialization**: The stored JSON in `sender.jsonSetting` is converted into a concrete setting class using Gson.
4. **Dispatch**: The router invokes the type-specific utility class (e.g., `TelegramUtils.sendMsg()`) to perform the network request.
5. **Result Handling**: `SendUtils.updateLogs()` writes the outcome to the log table, and `SendUtils.senderLogic()` determines whether to proceed to the next sender in the chain.

## Step-by-Step Guide to Adding a New Forwarding Target

To add a custom sender (for example, Slack), follow this implementation pattern:

### Step 1: Define a Type Constant

Add a unique integer identifier to [`app/src/main/kotlin/cn/ppps/forwarder/utils/Constants.kt`](https://github.com/pppscn/SmsForwarder/blob/main/app/src/main/kotlin/cn/ppps/forwarder/utils/Constants.kt):

```kotlin
const val TYPE_SLACK = 99   // Choose an unused integer

```

### Step 2: Create the Setting Data Class

Define a data class in [`app/src/main/kotlin/cn/ppps/forwarder/entity/setting/SlackSetting.kt`](https://github.com/pppscn/SmsForwarder/blob/main/app/src/main/kotlin/cn/ppps/forwarder/entity/setting/SlackSetting.kt) that models all required API fields:

```kotlin
data class SlackSetting(
    val webhookUrl: String,
    val channel: String,
    val username: String? = null
)

```

### Step 3: Implement the Sender Utility

Create [`app/src/main/kotlin/cn/ppps/forwarder/utils/sender/SlackUtils.kt`](https://github.com/pppscn/SmsForwarder/blob/main/app/src/main/kotlin/cn/ppps/forwarder/utils/sender/SlackUtils.kt) with the network logic:

```kotlin
object SlackUtils {
    fun sendMsg(
        setting: SlackSetting,
        msgInfo: MsgInfo,
        rule: Rule,
        senderIndex: Int,
        logId: Long,
        msgId: Long
    ) {
        val payload = mapOf(
            "text" to "[${msgInfo.from}] ${msgInfo.content}",
            "channel" to setting.channel,
            "username" to setting.username
        )
        
        XHttp.post(setting.webhookUrl)
            .upJson(Gson().toJson(payload))
            .execute(object : SimpleCallBack<String>() {
                override fun onSuccess(t: String?) {
                    SendUtils.updateLogs(logId, 2, "Slack OK")
                    SendUtils.senderLogic(2, msgInfo, rule, senderIndex, msgId)
                }

                override fun onError(e: Throwable?) {
                    SendUtils.updateLogs(logId, 0, e?.message ?: "Slack error")
                    SendUtils.senderLogic(0, msgInfo, rule, senderIndex, msgId)
                }
            })
    }
}

```

### Step 4: Register the Router Branch

Add a dispatch case in `SendUtils.sendMsgSender()` within [`app/src/main/kotlin/cn/ppps/forwarder/utils/SendUtils.kt`](https://github.com/pppscn/SmsForwarder/blob/main/app/src/main/kotlin/cn/ppps/forwarder/utils/SendUtils.kt):

```kotlin
TYPE_SLACK -> {
    val settingVo = Gson().fromJson(sender.jsonSetting, SlackSetting::class.java)
    SlackUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId)
}

```

### Step 5: Build the UI Fragment (Optional)

For user configuration, create [`app/src/main/kotlin/cn/ppps/forwarder/fragment/senders/SlackFragment.kt`](https://github.com/pppscn/SmsForwarder/blob/main/app/src/main/kotlin/cn/ppps/forwarder/fragment/senders/SlackFragment.kt) along with a corresponding layout XML in `app/src/main/res/layout/`. The fragment should:
- Build a `SlackSetting` instance from form inputs
- Serialize it using `Gson().toJson(settingVo)`
- Store the resulting string in the `Sender` entity’s `jsonSetting` field
- Register the fragment in the sender list UI (typically `SenderFragment` or the navigation graph)

## Complete Implementation Example

Here is the full integration for adding Slack support to SmsForwarder:

```kotlin
// Constants.kt
const val TYPE_SLACK = 99

// SlackSetting.kt
data class SlackSetting(
    val webhookUrl: String,
    val channel: String,
    val username: String? = null
)

// SlackUtils.kt
object SlackUtils {
    fun sendMsg(setting: SlackSetting, msgInfo: MsgInfo,
                rule: Rule, senderIndex: Int,
                logId: Long, msgId: Long) {
        val payload = mapOf(
            "text" to "${msgInfo.from}: ${msgInfo.content}",
            "channel" to setting.channel,
            "username" to setting.username
        )
        XHttp.post(setting.webhookUrl)
            .upJson(Gson().toJson(payload))
            .execute(object : SimpleCallBack<String>() {
                override fun onSuccess(t: String?) {
                    SendUtils.updateLogs(logId, 2, "OK")
                    SendUtils.senderLogic(2, msgInfo, rule, senderIndex, msgId)
                }
                override fun onError(e: Throwable?) {
                    SendUtils.updateLogs(logId, 0, e?.message ?: "Error")
                    SendUtils.senderLogic(0, msgInfo, rule, senderIndex, msgId)
                }
            })
    }
}

// Addition to SendUtils.sendMsgSender()
TYPE_SLACK -> {
    val settingVo = Gson().fromJson(sender.jsonSetting, SlackSetting::class.java)
    SlackUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId)
}

```

## Summary

- **SmsForwarder sender architecture** uses a data-driven router that decouples message handling from transport implementation.
- **Four components** are required to add a new target: a type constant in [`Constants.kt`](https://github.com/pppscn/SmsForwarder/blob/main/Constants.kt), a settings data class, a utility class with `sendMsg()`, and a router branch in [`SendUtils.kt`](https://github.com/pppscn/SmsForwarder/blob/main/SendUtils.kt).
- **Background workers** (`SendWorker` and `SendLogicWorker`) and the rule engine require no modifications when adding new sender types.
- **JSON serialization** via Gson bridges the database storage and runtime configuration objects.
- **Sender logic** (ALL, UNTIL_SUCCESS, UNTIL_FAIL, RETRY) is evaluated by `SendUtils.senderLogic()` after each send attempt, enabling complex retry and fallback strategies.

## Frequently Asked Questions

### Do I need to modify the background workers to add a new sender?

No. `SendWorker` and `SendLogicWorker` operate on generic `Sender` objects and invoke `SendUtils.sendMsgSender()` without knowing specific sender types. Because the router handles type dispatch, new senders automatically work with the existing background processing system.

### What is senderLogic and how does it affect forwarding?

**SenderLogic** defines how multiple senders in a rule execute. The `ALL` mode sends to every configured target regardless of success, `UNTIL_SUCCESS` stops after the first successful delivery, `UNTIL_FAIL` stops after the first failure, and `RETRY` enables automatic re-queuing. This logic is processed in `SendUtils.senderLogic()` after each sender completes.

### How does SmsForwarder handle JSON serialization for settings?

The app stores sender configurations as JSON strings in the `Sender.jsonSetting` database column. When routing occurs, `SendUtils.sendMsgSender()` uses `Gson().fromJson()` to deserialize the string into the specific setting data class (e.g., `SlackSetting`) based on the sender's type constant.

### Can I add a sender without creating a UI fragment?

Yes. The UI fragment in `app/src/main/kotlin/cn/ppps/forwarder/fragment/senders/` is optional if you populate the `Sender` entity programmatically or via a custom configuration method. The only required components are the type constant, setting data class, utility implementation, and the router branch in [`SendUtils.kt`](https://github.com/pppscn/SmsForwarder/blob/main/SendUtils.kt).