How SmsForwarder Processes SMS Messages and Routes Them Through Rules to Senders
SmsForwarder processes incoming SMS messages by capturing system broadcasts in SmsReceiver, converting them into MsgInfo objects, and routing them through a multi-stage WorkManager pipeline that evaluates user-defined rules in ActionWorker before dispatching to concrete sender implementations like Telegram or HTTP APIs.
SmsForwarder is an open-source Android application that intercepts SMS and MMS broadcasts and forwards them to external services based on user-configurable rules. The architecture cleanly separates message reception from rule evaluation and delivery through a chain of CoroutineWorker implementations. This article examines the exact mechanism by which the app processes raw SMS data and routes it through the rule engine to sender implementations.
Receiving SMS and MMS via SmsReceiver
The entry point for all message processing is SmsReceiver, a BroadcastReceiver registered for system intents in app/src/main/kotlin/cn/ppps/forwarder/receiver/SmsReceiver.kt. It listens for four primary broadcast actions:
Telephony.Sms.Intents.SMS_RECEIVED_ACTION
Telephony.Sms.Intents.SMS_DELIVER_ACTION
Telephony.Sms.Intents.WAP_PUSH_RECEIVED_ACTION
Telephony.Sms.Intents.WAP_PUSH_DELIVER_ACTION
When a broadcast arrives, the receiver first filters out unwanted actions and checks global feature toggles via SettingUtils.enablePureClientMode and SettingUtils.enableSms. It then extracts the sender address and message body, handling SMS and MMS payloads through distinct code paths.
For plain SMS messages, the receiver iterates through Telephony.Sms.Intents.getMessagesFromIntent. For MMS, it extracts the raw PDU and parses each part in the handleMmsData method, capturing both text and image attachments. The receiver identifies the SIM slot using intent extras (slot, simId, subscription) or falls back to PhoneUtils.getSimMultiInfo if these fields are unavailable.
The receiver constructs a MsgInfo object that encapsulates all relevant metadata:
val msgInfo = MsgInfo(
"sms", // type
from, // sender address
msg, // full text (SMS + MMS parts)
Date(), // receive time
simInfo, // string tag like "SIM1_extra"
simSlot, // 0/1/‑1
subscription // Android subscription id
)
Finally, it serializes the MsgInfo to JSON and enqueues a SendWorker using Android's WorkManager:
val request = OneTimeWorkRequestBuilder<SendWorker>()
.setInputData(workDataOf(Worker.SEND_MSG_INFO to Gson().toJson(msgInfo)))
.build()
WorkManager.getInstance(context).enqueue(request)
Dispatching to SendWorker for Rule Matching
SendWorker (located in app/src/main/kotlin/cn/ppps/forwarder/workers/SendWorker.kt) operates as a CoroutineWorker that bridges message reception and rule execution. It performs three critical functions:
- Deserializes the incoming
MsgInfoJSON from its input data - Queries the rule database via
RuleDaoorRuleRepositoryto load all user-defined forwarding rules - Filters applicable rules by checking if the message matches enabled rules based on SIM slot, sender number, content keywords, and time ranges
Each rule in SmsForwarder consists of conditions and actions wrapped in TaskSetting objects. When SendWorker identifies a matching rule, it creates a separate ActionWorker request for that specific rule, passing both the original MsgInfo and the rule configuration:
val actionRequest = OneTimeWorkRequestBuilder<ActionWorker>()
.setInputData(
workDataOf(
Worker.SEND_MSG_INFO to gson.toJson(msgInfo),
Worker.RULE_JSON to gson.toJson(rule)
)
)
.build()
WorkManager.getInstance(context).enqueue(actionRequest)
This design decouples rule selection from action execution, allowing multiple rules to process the same message concurrently.
Evaluating Conditions and Executing Actions in ActionWorker
ActionWorker (in app/src/main/kotlin/cn/ppps/forwarder/workers/ActionWorker.kt) serves as the core rule engine. It receives the MsgInfo and a specific rule JSON, then executes the following algorithm.
First, it deserializes the rule and extracts the condition and action lists:
val rule = Gson().fromJson(ruleJson, Rule::class.java)
val conditionList = Gson().fromJson(taskConditionsJson, Array<TaskSetting>::class.java).toMutableList()
val actionList = Gson().fromJson(taskActionsJson, Array<TaskSetting>::class.java).toMutableList()
The worker then evaluates each condition in sequence. For every TaskSetting in the condition list, it loads the concrete setting class (such as Rule, SimSetting, or SenderSetting) and invokes the corresponding judge method, typically RuleUtils.judge(ruleSetting, msgInfo). If any condition evaluates to false, the worker returns immediately and skips action execution.
When all conditions succeed, the worker iterates through the action list. The most common action type is the sender action, which triggers message forwarding. The worker uses SenderFactory.create(senderSetting) to instantiate the appropriate sender class and calls sender.send(msgInfo) to dispatch the message.
Sender Implementations and Message Delivery
Concrete sender classes reside in app/src/main/kotlin/cn/ppps/forwarder/sender/ and implement a common interface (typically ISender). Each sender receives the complete MsgInfo object containing the original text, SIM slot metadata, and any MMS attachments.
- TelegramSender forwards messages via the Telegram Bot API using
sendMessagefor text andsendPhotofor image attachments - HttpSender constructs generic HTTP POST requests with JSON payloads to user-defined endpoints
- SmsSender utilizes Android's
SmsManagerto forward messages to another phone number - EmailSender handles SMTP transport via third-party libraries
All senders consume the same MsgInfo structure, ensuring consistent access to message content regardless of the transport method.
Summary
SmsReceivercaptures system SMS/MMS broadcasts, extracts payload data, resolves SIM slot information, and encapsulates everything in aMsgInfoobject before enqueueingSendWorkerSendWorkerdeserializes the message, queries the rule database, filters matching rules based on conditions like SIM slot and keywords, and schedules individualActionWorkerinstances for each matchActionWorkerevaluates rule conditions usingTaskSettingobjects andRuleUtils.judge(); upon successful validation, it executes actions by instantiating sender classes viaSenderFactory- Sender implementations (Telegram, HTTP, SMS, Email) receive the
MsgInfoand handle the final transport layer communication, completing the forwarding pipeline
Frequently Asked Questions
How does SmsForwarder determine which SIM card received the message?
SmsForwarder extracts SIM slot information from intent extras (slot, simId, subscription) provided by the Android system in SmsReceiver.kt. If these fields are unavailable or ambiguous, it falls back to PhoneUtils.getSimMultiInfo to query the device's SIM information and resolve the correct slot identifier.
What happens if multiple rules match the same incoming SMS?
When SendWorker identifies multiple matching rules, it creates a separate ActionWorker request for each rule and enqueues them concurrently via WorkManager. Each rule executes independently, meaning the same message can be forwarded to multiple destinations (e.g., Telegram and HTTP endpoint) simultaneously if configured.
How are rule conditions evaluated against incoming messages?
ActionWorker deserializes the rule's condition list into TaskSetting objects. It then calls specific judge methods like RuleUtils.judge() to compare the MsgInfo data (sender number, content, SIM slot) against the rule criteria. All conditions must evaluate to true for actions to execute.
Can SmsForwarder handle MMS attachments like images?
Yes. In SmsReceiver.kt, the handleMmsData method parses the raw PDU from WAP_PUSH_RECEIVED_ACTION intents to extract both text and binary parts. The MsgInfo object can carry attachment data, and senders like TelegramSender use sendPhoto to transmit images via the Telegram Bot API alongside text content.
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 →