iMessage
Native macOS iMessage integration for Aleph via Messages.app SQLite polling, plus a cross-platform BlueBubbles transport
The iMessage interface provides native macOS integration by reading messages from the Messages.app SQLite database and sending replies via AppleScript, plus a cross-platform BlueBubbles transport that talks REST + webhook to a running BlueBubbles server. Both transports implement the same Channel trait and are routed through the same inbound pipeline; you can run either (or both) from a single config.
Two Transports
┌──────────────────────────────────────────────────────────────────┐
│ macOS System │
│ ┌──────────────────┐ ┌──────────────────────────────────┐ │
│ │ Messages.app │ │ Aleph Server │ │
│ │ │ │ │ │
│ │ chat.db ────────┼─────┼──> SQLite Polling (read-only) │ │
│ │ (SQLite) │ │ [IMessageChannel, macOS-only] │ │
│ │ │ <───┼─── AppleScript (send message) │ │
│ └──────────────────┘ └──────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ BlueBubbles Server (any host) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ REST API + webhook ◀──▶ [BlueBubblesChannel, any OS] │ │
│ └──────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘IMessageChannel(local) — SQLite database polling for receiving, AppleScript for sending. Requires macOS, Full Disk Access, and Automation permission. Only registered on macOS (#[cfg(target_os = "macos")]on the module).BlueBubblesChannel(imessage/bluebubbles/) — REST + webhook against a BlueBubbles server. Pure HTTP — compiles and runs on any OS.
Adapter Structure
The iMessage adapter lives under src/gateway/interfaces/imessage/:
| Module | Responsibility |
|---|---|
mod.rs | IMessageChannel, capability defaults, factory wiring |
config.rs | IMessageConfig, DmPolicy, GroupPolicy |
db.rs | MessagesDb — read-only SQLite wrapper around chat.db |
sender.rs | AppleScript-driven outbound send |
target.rs | Target normalization (normalize_phone, parse_target, IMessageTarget, Service) |
reaction.rs | Tapback code ↔ emoji mapping (single source for both transports) |
bluebubbles/ | BlueBubblesChannel + REST/webhook staging |
Both transports share the gateway-shared OffsetTracker (currently homed in the Telegram module) for the catch-up watermark so that messages arriving while the daemon is offline are picked up on restart.
Capabilities
IMessageChannel (Local)
| Feature | Status |
|---|---|
| Text messages | Supported |
| Rich text (Markdown) | Not supported (plain text only) |
| Photos | Supported (send and receive) |
| Documents | Supported (send and receive) |
| Audio/Video | Supported (send and receive) |
| Tapback reactions | Inbound add-tapbacks are surfaced to the model as context; outbound is unreliable across macOS versions |
| Reply threading | Not supported |
| Message editing | Not supported |
| Message deletion | Not supported |
| Typing indicator | Not supported |
| Read receipts | Not supported |
| Max message length | ~20,000 characters |
| Max attachment size | 100 MB |
| Stream protocol | None |
BlueBubblesChannel
| Feature | Status |
|---|---|
| Text messages | Supported |
| Rich text (Markdown) | Not supported |
| Photos | Supported (send and receive) |
| Documents | Supported (send and receive) |
| Audio/Video | Supported (send and receive) |
| Tapback reactions | Supported (inbound + outbound via BlueBubbles API) |
| Reply threading | Supported |
| Message editing | Not supported |
| Message deletion | Not supported |
| Typing indicator | Supported |
| Read receipts | Supported |
| Max message length | 4,000 characters |
| Max attachment size | 100 MB |
| Stream protocol | None |
Prerequisites
Local Transport
macOS Requirement
The local transport is only registered on macOS (#[cfg(target_os = "macos")]). On Linux/Windows builds, only BlueBubblesChannel is available.
Full Disk Access
Aleph needs Full Disk Access permission to read ~/Library/Messages/chat.db:
- Open System Settings > Privacy & Security > Full Disk Access
- Click the + button
- Add the Aleph server binary (or Terminal.app if running from terminal)
- Restart the Aleph server
Without this permission, the database open will fail with a "permission denied" error.
Automation Permission
Aleph needs Automation permission to control Messages.app via AppleScript:
- The first time Aleph sends a message, macOS will show a permission dialog
- Click Allow to grant Automation access
- You can manage this in System Settings > Privacy & Security > Automation
BlueBubbles Transport
Run a BlueBubbles Server
You need a running BlueBubbles server (typically on the same Mac that owns the iMessage account). Install and start it per the BlueBubbles docs; note the server URL and the account password.
Open the REST + Webhook Ports
Make sure Aleph can reach the BlueBubbles server (HTTPS) and that the BlueBubbles webhook can reach Aleph (the gateway's HTTP server).
Configuration
Local Transport
[[channels]]
id = "imessage-local"
channel_type = "imessage"
enabled = true
[channels.config]
# Path to the Messages database (default: ~/Library/Messages/chat.db)
db_path = "~/Library/Messages/chat.db"
# Poll interval in milliseconds (default: 1000)
poll_interval_ms = 1000
# DM policy: pairing | allowlist | open | disabled
dm_policy = "pairing"
# Group message policy: open | allowlist | disabled
group_policy = "open"
# Allowlist of phone numbers/emails for DMs
allow_from = ["+15551234567", "user@example.com"]
# Allowlist for group chats (by chat identifier)
group_allow_from = []
# Require @mention in group chats (default: true)
require_mention = true
# Bot's name for mention detection in groups
bot_name = "Aleph"
# Include attachments in inbound messages (default: true)
include_attachments = true
# Maximum attachment size in bytes (0 = unlimited)
max_attachment_size = 0
# Inbound message debounce in milliseconds (default: 500)
inbound_debounce_ms = 500BlueBubbles Transport
[[channels]]
id = "imessage-bb"
channel_type = "imessage.bluebubbles"
enabled = true
[channels.config]
# BlueBubbles server URL (HTTPS)
server_url = "https://bluebubbles.local:1234"
# BlueBubbles account password
password = "${BLUEBUBBLES_PASSWORD}"
# Optional webhook URL (set on the BlueBubbles server side)
webhook_url = "https://aleph.example.com/webhooks/imessage"
# Poll interval in milliseconds for the fallback poll loop
poll_interval_ms = 1000
# Same DM / group / allowlist policy as the local transport
dm_policy = "pairing"
group_policy = "open"
allow_from = ["+15551234567"]DM and Group Policy
The dm_policy setting controls how Aleph handles direct messages from unknown senders:
| Policy | Behavior |
|---|---|
pairing (default) | Unknown senders are prompted for a pairing code before Aleph responds |
allowlist | Only senders in allow_from are processed; all others are ignored |
open | All senders are accepted without restriction |
disabled | DMs are completely disabled |
The pairing policy uses the gateway's single PairingStore. An operator generates a pairing code, the unknown sender sends that code via iMessage, and the operator approves it from the Panel via channel.pairing.approve. The phone number or email is added to the allowlist on approval. There is no per-channel pairing database.
Group Policy
| Policy | Behavior |
|---|---|
open (default) | All group chats are accepted (with optional mention requirement) |
allowlist | Only groups in group_allow_from are processed |
disabled | Group messages are completely disabled |
How Message Polling Works
Local Transport (IMessageChannel)
The polling loop runs in a background Tokio task:
- Open database —
chat.dbis opened read-only (SQLITE_OPEN_READ_ONLY) - Record last ROWID — On startup, the current max ROWID is the baseline
- Poll loop — Every
poll_interval_ms:- Query for messages with
ROWID > last_seenandis_from_me = 0 - For each new message, resolve the sender handle and chat info
- Extract attachments if
cache_has_attachmentsis set - Convert to an
InboundMessageand broadcast through the channel state
- Query for messages with
- Update marker — The last seen ROWID is updated after each batch
BlueBubbles Transport (BlueBubblesChannel)
BlueBubblesChannel registers a webhook with the BlueBubbles server for push delivery and falls back to a poll loop (poll_interval_ms) for catch-up. The shared OffsetTracker makes both transports resume after restart from the last persisted GUID/ROWID instead of jumping straight to the newest message.
Apple Timestamps
Messages.app uses "Apple Cocoa Core Data timestamps" — nanoseconds since January 1, 2001 UTC. Aleph converts these to standard Unix timestamps:
unix_timestamp = apple_timestamp / 1,000,000,000 + 978,307,200Sending Messages
Local Transport — AppleScript
tell application "Messages"
set targetService to 1st account whose service type = iMessage
set targetBuddy to participant "+15551234567" of targetService
send "Hello from Aleph!" to targetBuddy
end tellGroup chats are addressed by chat ID:
tell application "Messages"
set targetChat to chat id "chat123456"
send "Hello group!" to targetChat
end tellBlueBubbles Transport — REST
The BlueBubbles transport sends through the BlueBubbles REST API; no AppleScript is required, so BlueBubbles works on any host and from non-Mac accounts. Reactions, replies, typing indicators, and read receipts go through the same REST surface.
Tapback Reactions
The single source for tapback codes (src/gateway/interfaces/imessage/reaction.rs) maps Apple/BlueBubbles numeric associatedMessageType values to canonical names and emoji:
| Code | Name | Emoji |
|---|---|---|
| 2000 | love | ❤️ |
| 2001 | like | 👍 |
| 2002 | dislike | 👎 |
| 2003 | laugh | 😂 |
| 2004 | emphasize | ‼️ |
| 2005 | question | ❓ |
The "remove" variants (3000–3005) and unknown codes are intentionally absent, so any emoji lookup against them yields None and the reaction is dropped at the mapping layer — this is how "only surface add tapbacks" is enforced without a second filter. Inbound add tapbacks are surfaced to the model as context (the user's reaction to a prior assistant message); remove tapbacks are dropped.
Media Handling
Receiving Attachments
Attachments are detected via the cache_has_attachments flag in the message table and resolved through message_attachment_join (local transport) or the BlueBubbles attachment metadata (BlueBubbles transport):
| Field | Source |
|---|---|
id | Attachment GUID |
mime_type | From attachment.mime_type (local) or BlueBubbles TransferName |
filename | From attachment.filename or transfer_name |
size | From attachment.total_bytes |
Sending Attachments
The local transport requires local file paths (sent via AppleScript send POSIX file). The BlueBubbles transport uploads through the BlueBubbles REST API.
Session Routing
| Context | Session Key |
|---|---|
DM with +15551234567 | agent:main:dm:+15551234567 or agent:main:imessage:dm:+15551234567 |
| Group chat (chat ID) | agent:main:imessage:group:{chat_identifier} |
The chat_identifier is typically the phone number for DMs or an internal ID for group chats.
Database Schema (Local Transport)
Aleph reads from three primary tables in chat.db:
message Table
| Column | Type | Purpose |
|---|---|---|
ROWID | INTEGER | Auto-incrementing message ID |
guid | TEXT | Unique message identifier |
text | TEXT | Message body |
handle_id | INTEGER | FK to handle table |
date | INTEGER | Apple timestamp (nanoseconds) |
is_from_me | INTEGER | 1 if sent by the Mac owner |
cache_has_attachments | INTEGER | 1 if message has attachments |
handle Table
| Column | Type | Purpose |
|---|---|---|
ROWID | INTEGER | Handle ID |
id | TEXT | Phone number or email |
service | TEXT | "iMessage" or "SMS" |
chat Table
| Column | Type | Purpose |
|---|---|---|
ROWID | INTEGER | Chat ID |
guid | TEXT | Chat GUID |
chat_identifier | TEXT | Phone number or group ID |
display_name | TEXT | Group name (if set) |
group_id | TEXT | Non-null for group chats |
Limitations
Local Transport
- macOS only — Cannot run on Linux or Windows
- Full Disk Access required — Database is protected by macOS TCC
- No real-time delivery — Polling-based with configurable interval (minimum practical: ~500ms)
- No rich text — AppleScript sends plain text only
- No typing indicator — Would require deeper system integration
- Outbound tapbacks unreliable — Sending tapbacks via AppleScript is not reliably supported across macOS versions
- No message editing/deletion — AppleScript does not expose these operations
BlueBubbles Transport
- Requires a BlueBubbles server — Run it on a Mac with an iMessage account; Aleph talks to it over HTTPS
- No rich text — Plain text only
- No message editing/deletion — BlueBubbles REST API does not expose these operations
Troubleshooting
| Problem | Solution |
|---|---|
| "Failed to open Messages database" | Grant Full Disk Access to the Aleph binary in System Settings (local transport) |
| "AppleScript execution failed" | Grant Automation permission; ensure Messages.app is installed and signed in |
| Bot does not see new messages | Verify db_path points to the correct database; check that is_from_me = 0 filter is working |
| Messages.app opens unexpectedly | This is normal — AppleScript may activate the app when sending |
| Slow response time | Decrease poll_interval_ms (e.g., to 500); note this increases SQLite read frequency |
| "Invalid target" error | Ensure the recipient is a valid phone number (with country code) or email address |
| Group messages ignored | Check group_policy and group_allow_from; verify require_mention settings |
| BlueBubbles: webhook not received | Verify webhook_url is reachable from the BlueBubbles host; check the Aleph gateway HTTP port |