Overview
The iMessage transport (internal/transport/imessage/) does not touch iMessage directly — Apple provides no supported client-facing API. Instead it polls BlueBubbles, a macOS-side daemon that exposes iMessage via HTTP + Socket.IO.
rousseau uses BlueBubbles's HTTP endpoints only (Socket.IO is deliberately avoided to keep the dependency footprint small).
Architecture
+-----------+ iMessage +---------+ HTTP +-----------+
| Apple ID | <---------------> | macOS | <-----------> | rousseau |
| server | | Blue | | daemon |
+-----------+ | Bubbles | | |
+---------+ +-----------+
The macOS host runs BlueBubbles and stays signed into iMessage. rousseau polls BlueBubbles's /api/v1/message endpoint on the configured cadence and forwards new arrivals to the handler.
Prerequisites
- A macOS host with iMessage signed in. Not necessarily the same machine rousseau runs on.
- BlueBubbles server installed on that host, listening on a URL rousseau can reach (LAN address, VPN, or Tailscale).
- BlueBubbles password from the server GUI (Settings → Server Password).
- A chat GUID for outbound. Find it in BlueBubbles's GUI or via
GET /api/v1/chat.
Configuration
imessage:
base_url: "http://mac.internal:1234"
password: "..."
chat_guid: "iMessage;-;+15550001234"
poll_interval: "5s"
reply_header: ""
| Field | Default | Effect |
|---|---|---|
base_url |
required | BlueBubbles server URL. |
password |
required | BlueBubbles server password. |
chat_guid |
empty | Outbound target GUID. |
poll_interval |
5s |
Poll cadence against /api/v1/message. |
reply_header |
empty | Prepended to every outbound message. |
Command-line
rousseau imessage \
--base-url http://mac.internal:1234 \
--password ... \
--chat-guid 'iMessage;-;+15550001234' \
--poll-interval 5s
Cursor deduplication
On startup, the transport primes its lastID cursor to the newest existing message so the operator does not get spammed with the entire iMessage history. Every subsequent poll fetches the newest PageSize messages (default 25) and only forwards those newer than the cursor.
The cursor is in-memory. On restart, the cursor is re-primed from BlueBubbles — a small window of messages that arrived while the daemon was down will be missed. This is a deliberate trade-off; persistent cursor logic would require another table in the state store and iMessage delivery timestamps are not guaranteed monotonic across devices.
Reachability
BlueBubbles must be network-reachable from wherever rousseau runs. Common patterns:
- Same LAN.
http://<mac-lan-ip>:1234. - Tailscale.
http://mac.tailnet.ts.net:1234. Encrypts the link and works across NAT. - Reverse tunnel.
http://localhost:1234on the rousseau host with an SSH-Rtunnel from the Mac.
Do not expose BlueBubbles to the public internet unless you understand the auth model (a single password).
Failure modes
| Symptom | Fix |
|---|---|
imessage.prime_failed on startup |
BlueBubbles unreachable — check base_url and password. |
| Every historic message replays | lastID did not prime. Check permissions / auth. |
| Outbound messages silently dropped | Wrong chat_guid. Look it up via GET /api/v1/chat. |
| Messages arrive minutes late | Increase BlueBubbles's own polling frequency or lower poll_interval. |