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:

  1. Deserializes the incoming MsgInfo JSON from its input data
  2. Queries the rule database via RuleDao or RuleRepository to load all user-defined forwarding rules
  3. 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 sendMessage for text and sendPhoto for image attachments
  • HttpSender constructs generic HTTP POST requests with JSON payloads to user-defined endpoints
  • SmsSender utilizes Android's SmsManager to 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

  • SmsReceiver captures system SMS/MMS broadcasts, extracts payload data, resolves SIM slot information, and encapsulates everything in a MsgInfo object before enqueueing SendWorker
  • SendWorker deserializes the message, queries the rule database, filters matching rules based on conditions like SIM slot and keywords, and schedules individual ActionWorker instances for each match
  • ActionWorker evaluates rule conditions using TaskSetting objects and RuleUtils.judge(); upon successful validation, it executes actions by instantiating sender classes via SenderFactory
  • Sender implementations (Telegram, HTTP, SMS, Email) receive the MsgInfo and 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →