SmsForwarder Sender Architecture: How to Add New Forwarding Targets
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, 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 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:
- Rule Matching:
SendLogicWorkerbuilds aMsgInfoobject and callsSendUtils.sendMsgSender(msgInfo, rule, senderIndex, logId, msgId). - Sender Selection: The router extracts the sender at
rule.senderList[senderIndex]. - Configuration Deserialization: The stored JSON in
sender.jsonSettingis converted into a concrete setting class using Gson. - Dispatch: The router invokes the type-specific utility class (e.g.,
TelegramUtils.sendMsg()) to perform the network request. - Result Handling:
SendUtils.updateLogs()writes the outcome to the log table, andSendUtils.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:
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 that models all required API fields:
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 with the network logic:
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:
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 along with a corresponding layout XML in app/src/main/res/layout/. The fragment should:
- Build a
SlackSettinginstance from form inputs - Serialize it using
Gson().toJson(settingVo) - Store the resulting string in the
Senderentity’sjsonSettingfield - Register the fragment in the sender list UI (typically
SenderFragmentor the navigation graph)
Complete Implementation Example
Here is the full integration for adding Slack support to SmsForwarder:
// 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, a settings data class, a utility class withsendMsg(), and a router branch inSendUtils.kt. - Background workers (
SendWorkerandSendLogicWorker) 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →