Skip to content

heyfinal/imsg-bridge

Repository files navigation

imsg-bridge

A lightweight REST + WebSocket bridge for iMessage on macOS.
Send and receive messages programmatically. No Electron. No Firebase. Just a thin API.

Quick StartAPIWebSocketDeploymentHow It Works


Why

Existing iMessage automation tools are either bloated (BlueBubbles, 200MB+ Electron app) or painfully slow (AppleScript with 120s+ send times). imsg-bridge wraps the imsg CLI — which talks directly to Apple's private iMessage frameworks — and exposes a clean async API that sends messages in under a second.

BlueBubbles AppleScript imsg-bridge
Send latency ~2s ~120s <1s
Memory ~200MB ~50MB ~30MB
Dependencies Electron, Firebase osascript FastAPI, uvicorn
Real-time receive Polling None WebSocket stream

Prerequisites

  • macOS (Sonoma / macOS 14+ recommended)
  • Python 3.12+
  • imsg CLI installed at /opt/homebrew/bin/imsg
  • iMessage signed in and working in Messages.app
  • Full Disk Access granted to your terminal app and Python (for chat.db reads)

Install

Preferred: AI-assisted install (Claude Code / Codex / Gemini)

Use a one-liner like:

claude -p "Install, configure, test, and optionally deploy imsg-bridge from https://github.com/heyfinal/imsg-bridge.git on this macOS machine."

Or:

codex exec "Install, configure, test, and optionally deploy imsg-bridge from https://github.com/heyfinal/imsg-bridge.git on this macOS machine."
gemini -p "Install, configure, test, and optionally deploy imsg-bridge from https://github.com/heyfinal/imsg-bridge.git on this macOS machine."

If you already have the repo locally, generate a tool-specific one-liner and copy it to clipboard:

./setup.sh --ai-prompt codex
# or: ./setup.sh --ai-prompt claude
# or: ./setup.sh --ai-prompt gemini

If you are already inside an interactive AI terminal, generate a plain copy/paste task prompt:

./setup.sh --ai-task

Paste this inside Claude/Codex/Gemini interactive mode:

Install, configure, test, and optionally deploy imsg-bridge from https://github.com/heyfinal/imsg-bridge.git on this macOS machine.

Manual one-liner (no package manager needed)

curl -fsSL https://raw.githubusercontent.com/heyfinal/imsg-bridge/main/install.sh | bash

This clones the repo to ~/.imsg-bridge, creates a virtual environment, and installs dependencies using only Python's built-in venv and pip. No Homebrew, no uv, no global installs.

Manual: with uv

git clone https://github.com/heyfinal/imsg-bridge.git
cd imsg-bridge
uv sync

Manual: with pip

git clone https://github.com/heyfinal/imsg-bridge.git
cd imsg-bridge
python3 -m venv .venv
.venv/bin/pip install -e .

Quick Start

cd imsg-bridge  # or ~/.imsg-bridge if you used the one-liner

# Generate auth token and install as a background service
./setup.sh

# Or run manually
imsg-bridge
# → Listening on configured host:5100

API

All endpoints require a bearer token in the Authorization header:

Authorization: Bearer <your-token>

The token is generated by setup.sh and stored in your macOS Keychain.

Send a message

curl -X POST http://127.0.0.1:5100/send \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"to": "+15551234567", "text": "Hello from the bridge"}'

List chats

curl http://127.0.0.1:5100/chats \
  -H "Authorization: Bearer $TOKEN"

Get message history

curl "http://127.0.0.1:5100/history/23?limit=50" \
  -H "Authorization: Bearer $TOKEN"

Health check

curl http://127.0.0.1:5100/health \
  -H "Authorization: Bearer $TOKEN"

Returns {"status": "ok", "imsg_version": "<detected-version>"} when everything is working.

Ping (unauthenticated)

curl http://127.0.0.1:5100/ping

Returns {"status": "ok"} — useful for monitoring, load balancers, and launchd health checks.

Resolve contact name

curl "http://127.0.0.1:5100/contact-name?identifier=%2B15551234567" \
  -H "Authorization: Bearer $TOKEN"

Returns {"identifier": "+15551234567", "name": "John Doe"} from the macOS Contacts database.

Get contact avatar

curl "http://127.0.0.1:5100/avatar?identifier=%2B15551234567" \
  -H "Authorization: Bearer $TOKEN" --output avatar.jpg

Returns the contact's photo as image/jpeg, image/png, or image/tiff. Looks across all macOS AddressBook source databases (top-level + each iCloud / Exchange / CardDAV source under Sources/<UUID>/) — required when iCloud Contacts is the primary store.

Fetch a message attachment

curl "http://127.0.0.1:5100/attachment?path=$(python3 -c 'import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))' '~/Library/Messages/Attachments/.../photo.jpg')" \
  -H "Authorization: Bearer $TOKEN" --output photo.jpg

Serves the file bytes for a Mac-local attachment path so non-Mac clients (e.g. the Android app) can display inline images, video, audio, and documents. Path-traversal blocked — only files under ~/Library/Messages/Attachments, /var/folders, or /tmp are accepted.

Contacts health check

curl "http://127.0.0.1:5100/contacts-status" -H "Authorization: Bearer $TOKEN"

Returns the AddressBook state so clients can surface a "no contacts found — enable iCloud Contacts" advisory when names and photos won't resolve:

{
  "healthy": true,
  "contact_count": 3089,
  "phone_count": 3208,
  "email_count": 334,
  "image_count": 429,
  "readable_databases": 2,
  "advisory": ""
}

WebSocket

Connect to /ws for a real-time stream of incoming messages:

websocat -H "Authorization: Bearer $TOKEN" "ws://127.0.0.1:5100/ws"

Each message arrives as a JSON object:

{
  "id": 43327,
  "guid": "A1B2C3D4-...",
  "chat_id": 23,
  "text": "Hey, what's up?",
  "sender": "+15551234567",
  "is_from_me": false,
  "created_at": "2026-03-03T06:00:00.000Z",
  "attachments": [],
  "reactions": []
}

Authentication via Authorization: Bearer <token> header.

For backwards compatibility with query-string tokens (less secure), you can set IMSG_BRIDGE_ALLOW_WS_QUERY_TOKEN=1.

Deployment

As a launchd service (recommended)

The setup.sh script handles everything:

./setup.sh

This will:

  1. Generate a secure auth token and store it in macOS Keychain
  2. Let you choose bind mode (0.0.0.0 for LAN clients or 127.0.0.1 for local-only)
  3. Install a LaunchAgent that starts on login and auto-restarts on crash
  4. Optionally deploy Linux client via:
    • LAN scan for SSH hosts (or manual IP entry) + username/password prompt
    • USB/external drive scan (or manual mount path)
  5. If Messages DB access is blocked, automatically open Full Disk Access settings and walk you through enabling it
  6. Verify the service is running

Logs are written to ~/Library/Logs/imessage-bridge.log and ~/Library/Logs/imessage-bridge.err.

LAN access (optional)

By default, the LaunchAgent binds to 127.0.0.1 (loopback only). If you want to access the bridge from other machines on your LAN (e.g. imsg-gtk on Linux), bind to 0.0.0.0 instead:

IMSG_BRIDGE_BIND_HOST=0.0.0.0 ./setup.sh

This exposes the bridge to your local network. Keep the bearer token private and consider using a firewall, Tailscale, or SSH tunneling.

Manual

imsg-bridge --host 127.0.0.1 --port 5100

Or with uvicorn directly:

uvicorn imsg_bridge.bridge:app --host 127.0.0.1 --port 5100

How It Works

Your app / bot / agent
    ↕ HTTP + WebSocket
imsg-bridge (FastAPI, port 5100)
    ↕ async subprocess
imsg CLI (/opt/homebrew/bin/imsg)
    ↕ private frameworks
~/Library/Messages/chat.db
  • REST endpoints spawn imsg subprocesses for each request (send, chats, history)
  • WebSocket runs a persistent imsg watch subprocess that streams new messages as JSONL
  • State persistence tracks the last processed message ROWID in ~/.imessage-bridge/state.json so no messages are lost across restarts
  • Auth uses a bearer token stored in macOS Keychain — no config files with secrets

Android Client (imsg_android)

A native Jetpack Compose tablet/phone app that mirrors macOS Messages.app over your LAN. Source lives in imsg_android/.

Features

  • Real iMessage-style UI: blue gradient bubbles (iMessage), green + 🔒 (encrypted RCS), green (SMS), with proper group-stack rounded corners
  • Contact name + photo resolution via the bridge (/contact-name, /avatar)
  • Pretty US phone formatting +14053151310 → (405) 315-1310 when no contact card matches
  • Reply quote bubbles above each reply, with sender name resolved through a cache
  • Inline image attachments via /attachment (Mac-local paths proxied with Bearer auth)
  • Day dividers (Wednesday Jun 10, 2026 8:06 AM), local-timezone correct
  • Smart auto-scroll (only follows when you're near the bottom)
  • Hardware Back navigation, dark/light mode, edge-to-edge
  • Persistent WebSocket foreground service with exponential-backoff reconnect; per-message notifications when the chat isn't visible

Requirements

  • Android 8.0+ (API 26+), tested on Samsung Galaxy Tab A7 Lite (Android 14)
  • Network access to the Mac running imsg-bridge
  • For builds: JDK 17, Android SDK platform 34, Gradle 8.10 (wrapper included)

Build & Install

cd imsg_android
JAVA_HOME=/path/to/openjdk@17 ./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk

Or use the one-shot installer that finds the device on LAN, USB, or by MAC:

./imsg_android/scripts/install_apk.sh

On first launch: open Settings → enter the bridge host (e.g. 192.168.1.211:5100) and Bearer token. The token is the same one stored in macOS Keychain under imessage-bridge.

If the chat list shows phone numbers instead of names and no photos, hit /contacts-status — if contact_count < 5, enable iCloud Contacts on the Mac (System Settings → Apple ID → iCloud → Contacts) and contacts will populate within ~5 minutes.

Linux Client (imsg-gtk)

A native GTK4/libadwaita desktop app for reading and sending iMessages from Linux over your LAN.

Features

  • Real-time message streaming via WebSocket
  • Contact name resolution and avatar display from macOS Contacts
  • Inline image attachments
  • Desktop notifications for incoming messages (when window is not focused)
  • Reconnection banner with automatic retry
  • Send failure indicator with visual feedback
  • Search/filter conversations
  • Right-click context menu (open, copy contact, clear)
  • Dark mode support via libadwaita

Requirements

  • Linux with GTK 4.10+, libadwaita 1.3+
  • Python 3.12+ with PyGObject
  • Network access to the Mac running imsg-bridge

Deploy

The easiest way is via setup.sh on the Mac, which offers SSH push or USB copy:

./setup.sh --deploy-ssh user@linux-machine
./setup.sh --deploy-usb /Volumes/USB

Or install manually on Linux:

cd imsg_gtk
pip install -e .
imsg-gtk

Configuration is stored in ~/.config/imsg-gtk/config.json:

{
  "host": "192.168.1.100",
  "port": 5100,
  "token": "your-bearer-token"
}

Security

  • Setup supports both bind modes:
    • 127.0.0.1 local-only (recommended default)
    • 0.0.0.0 LAN-accessible (required for direct Linux client access)
  • Bearer token required on all endpoints (REST and WebSocket) except /ping
  • Token stored in macOS Keychain, never on disk
  • Uses constant-time token comparison for auth checks
  • Rate limiting on /send (20 requests/minute sliding window)
  • imsg binary path configurable via IMSG_PATH environment variable
  • For remote access, use Tailscale or an SSH tunnel

Development

# Install dev tooling
python3 -m pip install -e ".[dev]"

# Optional: run checks automatically on commit
pre-commit install

# Lint + tests
ruff check imsg_bridge imsg_gtk tests
pytest -q

CI runs these checks on every push and pull request.

License

MIT