Skip to content

Email, iMessage, Linq & automation

Email (IMAP/SMTP & Gmail Push), macOS iMessage, Linq SMS/RCS, and webhook/MQTT/Notion automation channels.

This page is the reference for the channels that connect Revka to email, native messaging, and automation systems rather than to chat apps: Email over IMAP/SMTP, Gmail Push over Google Cloud Pub/Sub, native iMessage on macOS, Linq for iMessage/RCS/SMS, the Generic Webhook adapter, MQTT as a Standard Operating Procedure (SOP) event trigger, and the Notion task-queue channel. Reach for these when your agent needs to live in an inbox, drive a phone number, fan in IoT events, or be wired into an arbitrary external system.

Every channel here is configured under [channels_config] in ~/.revka/config.toml. Edit the file directly or run revka onboard --channels-only, then apply changes by restarting the daemon. For the shared trait model, delivery modes, and allowlist semantics, see Channels overview. If this is your first channel, read Connect a messaging channel first.

ChannelReceive modePublic inbound port?Allowlist field
Email (IMAP/SMTP)IMAP IDLENoallowed_senders
Gmail PushPub/Sub webhookYesallowed_senders
iMessagemacOS AppleScript bridgeNo (macOS only)allowed_contacts
LinqWebhook (push)Yesallowed_senders
Generic WebhookHTTP serverUsually yes
MQTTBroker subscriberNo (outbound connection)
NotionPollingNo— (access controlled by Notion integration sharing)

The webhook-based channels (Gmail Push, Linq) need your gateway to be reachable over public HTTPS — see Expose your gateway with a tunnel. The Generic Webhook channel runs its own HTTP listener on a configurable port. The rest connect outbound and work behind NAT.

The Email channel listens with IMAP IDLE for real-time push and sends over SMTP. Both legs use TLS (validated with rustls). It parses multipart MIME, gates senders by address or domain, and tracks replies through the Message-ID and In-Reply-To headers. Per RFC 2177, the IDLE connection is recycled on a ~29-minute cycle so it stays alive behind middleboxes. No public inbound port is required.

[channels_config.email]
imap_host = "imap.example.com"
imap_port = 993
imap_folder = "INBOX"
smtp_host = "smtp.example.com"
smtp_port = 465
smtp_tls = true
username = "[email protected]"
password = "email-password"
from_address = "[email protected]"
idle_timeout_secs = 1740
allowed_senders = ["*"]
default_subject = "Revka Message"
FieldTypeDefaultMeaning
imap_hoststringIMAP server hostname. Required.
imap_portnumber993IMAP port (implicit TLS).
imap_folderstring"INBOX"Mailbox folder to watch.
smtp_hoststringSMTP server hostname. Required.
smtp_portnumber465SMTP port (implicit TLS).
smtp_tlsbooltrueUse TLS for the SMTP connection.
usernamestringAccount username for IMAP and SMTP auth. Required.
passwordstringAccount password. Required. Use an app password for Gmail/Outlook.
from_addressstringAddress used in the From header on outbound mail. Required.
idle_timeout_secsnumber1740IDLE reconnect interval (1740 s = 29 min). Alias: poll_interval_secs.
allowed_sendersarrayEmail addresses or domains allowed to interact. [] denies all; ["*"] allows everyone.
default_subjectstring"Revka Message"Subject line used when the agent starts a new thread.

The Gmail Push channel replaces IMAP polling with Google Cloud Pub/Sub push: when a watched mailbox changes, Google posts a notification to your gateway, and Revka uses the Gmail History API to fetch the new messages. It watches the labels you configure (default INBOX) and auto-renews the Gmail watch subscription before its 7-day expiry.

This is a webhook channel: Google must reach POST /webhook/gmail on your gateway, so you need a public HTTPS URL.

[channels_config.gmail_push]
enabled = true
topic = "projects/my-project/topics/gmail-topic"
oauth_token = "" # or GMAIL_PUSH_OAUTH_TOKEN env var
label_filter = ["INBOX"]
allowed_senders = ["*"]
webhook_url = "https://example.com/webhook/gmail"
webhook_secret = "" # or GMAIL_PUSH_WEBHOOK_SECRET env var
FieldTypeDefaultMeaning
enabledboolfalseMaster switch for the channel.
topicstringFull GCP Pub/Sub topic path (projects/<id>/topics/<name>). Required.
oauth_tokenstringGmail API OAuth2 token. Falls back to GMAIL_PUSH_OAUTH_TOKEN.
label_filterarray["INBOX"]Gmail labels to watch.
allowed_sendersarrayEmail addresses or domains allowed to interact. [] denies all; ["*"] allows everyone.
webhook_urlstringPublic URL registered as the Pub/Sub push subscription endpoint.
webhook_secretstringBearer token used to authenticate incoming push requests. Falls back to GMAIL_PUSH_WEBHOOK_SECRET.

To wire up the push path:

  1. Create a GCP project and a Pub/Sub topic (its path is your topic value).
  2. Grant the Gmail push service account [email protected] the Pub/Sub Publisher role on that topic, so Gmail is allowed to publish change notifications.
  3. Create a push subscription on the topic whose endpoint is your webhook_url (https://<gateway>/webhook/gmail).
  4. Provide an oauth_token with Gmail API scope so Revka can register the watch and read message history.

Revka registers the Gmail watch and auto-retries renewal as it nears expiry. You can run Gmail Push to supplement or replace the IMAP Email channel for Gmail accounts.

The iMessage channel is macOS only. It polls the local Messages database at ~/Library/Messages/chat.db for new messages and sends replies by driving the Messages app through osascript (AppleScript). It reads both the modern attributedBody typedstream blobs used on Ventura and later, and the legacy text column on older macOS.

[channels_config.imessage]
allowed_contacts = ["*"]
FieldTypeDefaultMeaning
allowed_contactsarrayContact identifiers (phone numbers / Apple IDs) allowed to interact. [] denies all; ["*"] allows everyone.

Reading ~/Library/Messages/chat.db is protected by macOS privacy controls. The process that runs Revka must be granted Full Disk Access, or the channel cannot see incoming messages.

  1. Open System Settings → Privacy & Security → Full Disk Access.
  2. Add the binary or app that launches Revka. If you run Revka from a terminal, grant Full Disk Access to that terminal application (for example Terminal or iTerm); if you run it as a background service, grant it to the Revka binary itself.
  3. Quit and relaunch the granted application so the new permission takes effect, then restart the daemon.

Linq routes iMessage, RCS, and SMS through the Linq Partner V3 API, so a single phone number can reach Apple, Android, and carrier-SMS recipients without a Mac. It runs in webhook mode: Linq posts inbound messages to POST /linq on your gateway, and Revka sends replies to https://api.linqapp.com/api/partner/v3. Inbound requests are authenticated with an HMAC-SHA256 signature plus a 300-second timestamp-freshness check. You need a public HTTPS URL for the webhook.

[channels_config.linq]
api_token = "linq-partner-api-token"
from_phone = "+15551234567"
signing_secret = "optional-signing-secret"
allowed_senders = ["*"]
FieldTypeDefaultMeaning
api_tokenstringLinq Partner API token. Required.
from_phonestringSender phone number in E.164 format. Required.
signing_secretstringHMAC signing secret for inbound webhook verification. Overridden by REVKA_LINQ_SIGNING_SECRET.
allowed_sendersarraySender phone numbers (E.164) allowed to interact. [] denies all; ["*"] allows everyone.

Set your Linq dashboard’s webhook URL to the gateway ingress path:

POST /linq
Content-Type: application/json

When a signing secret is set, the gateway verifies the HMAC-SHA256 signature on each request and rejects payloads whose timestamp is more than 300 seconds old (replay protection). The secret resolves from the environment first, then config:

Terminal window
export REVKA_LINQ_SIGNING_SECRET="your-signing-secret"

Linq prevents self-echo by dropping messages where is_from_me is set or direction is "outbound"; both the legacy payload shape and the newer sender_handle.is_me form are recognized. Inbound media parts with an image/* MIME type are converted to [IMAGE:url] markers so the agent sees the attachment. For the full set of gateway ingress endpoints, see Webhook ingress.

The Generic Webhook channel is a universal adapter for any system that can POST JSON and (optionally) receive a callback. It runs its own HTTP listener on a configurable port, receives messages at a configurable path, and posts replies to an outbound URL. Inbound requests can be authenticated with an HMAC-SHA256 signature.

[channels_config.webhook]
port = 8080
path = "/webhook"
send_url = "https://target.example.com/callback"
send_method = "POST"
auth_header = "Bearer my-token"
secret = "my-hmac-secret"
FieldTypeDefaultMeaning
portnumberPort the webhook HTTP server listens on. Required.
pathstring"/webhook"Path that accepts inbound message POSTs.
send_urlstringOutbound callback URL for replies. Omit for receive-only operation.
send_methodstring"POST"HTTP method used for outbound replies (e.g. POST, PUT).
auth_headerstringValue sent as the Authorization header on outbound requests.
secretstringShared HMAC-SHA256 secret for verifying inbound signatures. No verification is performed when unset.

Inbound POST body:

{"sender": "user@example", "content": "hello", "thread_id": "opt-thread"}

Outbound reply body (sent to send_url):

{"content": "reply text", "thread_id": "opt", "recipient": "user@example"}

The optional thread_id carries through the round trip, letting external systems thread a conversation. If you do not set secret, no signature is required — always use HTTPS and a secret for any production deployment.

MQTT is different from the other channels on this page: it is not a chat channel. It subscribes to an MQTT broker and routes incoming messages into the Standard Operating Procedure (SOP) engine rather than the agent chat loop. Use it to fan IoT and automation events into SOP workflows. It connects outbound to the broker, so no inbound port is needed, and it supports TLS (mqtts://), authentication, and per-topic QoS.

In an SOP’s SOP.toml, declare an MQTT trigger with the topic to match and an optional JSONPath condition:

[[triggers]]
type = "mqtt"
topic = "sensors/pressure"
condition = "$.value > 85"

The topic supports the same + and # wildcards as a subscription, and condition is a JSONPath expression that fails closed — a missing or invalid payload does not match. For the full SOP trigger model (webhook, cron, peripheral, manual) and execution modes, see SOP reference: syntax, triggers & execution.

The Notion channel is a task queue, not a real-time chat channel. It polls a Notion database for rows whose status property is set to a pending value, dispatches each row’s prompt to the agent, and writes the result back into the row. On startup it can reset stale “running” rows so a crash mid-task is recoverable, and it caps how many tasks run concurrently.

[channels_config.notion]
api_key = "secret_..."
database_id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
poll_interval_secs = 5
status_property = "Status"
input_property = "Input"
result_property = "Result"
max_concurrent = 4
recover_stale = true
FieldTypeDefaultMeaning
api_keystringNotion integration secret. Required.
database_idstringTarget database ID. Required.
poll_interval_secsnumber5How often the database is polled for pending rows.
status_propertystring"Status"Property holding task status. Both select and status property types are supported.
input_propertystring"Input"Property holding the prompt text.
result_propertystring"Result"Property the agent’s result is written into.
max_concurrentnumber4Maximum tasks processed in parallel.
recover_stalebooltrueOn boot, reset rows stuck in “running” so they are reprocessed.

The sender-gating field differs per channel, but the rule is the same everywhere:

ChannelField
Email, Gmail Push, Linqallowed_senders
iMessageallowed_contacts
  • An empty list ([]) denies all senders.
  • A single "*" entry allows all senders.
  • Otherwise only listed identities are accepted. For email, an entry can be a full address or a bare domain.

The Generic Webhook and MQTT channels have no allowlist field: the webhook is gated by its HMAC secret, and MQTT trust comes from the broker connection and (optionally) username/password. The Notion channel has no sender allowlist — access is controlled by which Notion database the integration is shared with.

Start with the permissive ["*"] to confirm delivery end to end, then tighten to explicit identities.