?

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

  1. A macOS host with iMessage signed in. Not necessarily the same machine rousseau runs on.
  2. BlueBubbles server installed on that host, listening on a URL rousseau can reach (LAN address, VPN, or Tailscale).
  3. BlueBubbles password from the server GUI (Settings → Server Password).
  4. 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:1234 on the rousseau host with an SSH -R tunnel 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.

Type to search 150+ pages. Ranking: BM25 with title/description boost.