# How SmsForwarder Processes SMS Messages and Routes Them Through Rules to Senders

> Learn how SmsForwarder processes SMS messages using a WorkManager pipeline. Discover how it evaluates rules and routes SMS to Telegram, HTTP, and other sender implementations.

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

---

**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`](https://github.com/pppscn/SmsForwarder/blob/main/app/src/main/kotlin/cn/ppps/forwarder/receiver/SmsReceiver.kt). It listens for four primary broadcast actions:

```kotlin
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:

```kotlin
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:

```kotlin
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`](https://github.com/pppscn/SmsForwarder/blob/main/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:

```kotlin
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`](https://github.com/pppscn/SmsForwarder/blob/main/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:

```kotlin
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`](https://github.com/pppscn/SmsForwarder/blob/main/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`](https://github.com/pppscn/SmsForwarder/blob/main/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.