Repository navigation
Echo Inbox
Echo Inbox is the web page rhythm echo up serves at its API address: one list of every email, SMS, webhook, chat message, analytics event and payment call the site sent, with each message's HTML, text, headers, checks and attachments. This page covers opening it, searching it, keeping it across restarts, letting a client sign in to read it and releasing one email to that client for sign-off.
rhythm echo up # then open http://127.0.0.1:8025The inbox is part of Echo. There is nothing to install and it loads nothing from the internet: its scripts and styles are built into the binary.
The list holds every channel, newest first, 50 messages a page with an Older messages button. Each row has the kind (Email, Twilio SMS, Zapier webhook) and the action that sent it, the time, a title (the subject, the event or the first words), badges (1 error, 2 warnings, 1 file, the status), who it's from and to and a snippet.
Opening a message shows its tabs. A tab without content is hidden:
| Tab | Shows |
|---|---|
| HTML | The email as a reader sees it, in a sandboxed frame: no scripts, links open in a new tab, remote images blocked until you tick Load remote images for that message. |
| Text | The plain-text part, or an SMS, chat or push text. |
| Headers | The headers as stored, with credentials masked. |
| Raw | The MIME source, or the HTTP request. |
| JSON | The message as rhythm echo show <id> prints it. |
| Checks | The problems Echo found in it, with a count: an unfilled placeholder, a PHP warning, a link to staging. See Messages and checks. |
| Attachments | Each file's name, type, size and SHA-256, with a count. Echo never keeps a file's content. See Attachments. |
Delete (or the Delete key) removes the open message. The inbox follows Echo's event stream: a green dot says it's connected, new and changed messages appear without a reload and new rows flash once.
| Key | Does |
|---|---|
j, k, or the arrow keys |
Next or previous message |
/ |
Focus the search box; Escape leaves it |
| Delete | Delete the open message |
The page before Echo Inbox, the full HTML report of the session so far, is still there at /report.
The search box takes words (all of which must match) and fields:
| Search | Finds |
|---|---|
to:ann, from:shop.test
|
A part of a recipient (To, Cc or Bcc) or of the sender |
subject:"order shipped" |
A phrase in the subject (quotes group words) |
channel:sms, channel:email,sms
|
One channel, or any of several |
action:checkout |
Messages from an action, or from a run |
provider:twilio |
Messages to one service; rhythm echo providers lists the slugs |
check:content/dev-link, check:content
|
Messages with a check's finding, or any check in a group |
site:client-a |
A site's ID or host, whole or by whole dot-separated labels: client-a and client-a.test, never client-ab
|
status:blocked |
Messages with this status |
has:attachment, has:issue, has:html
|
Messages with files, with findings or with HTML |
-channel:analytics |
A minus sign excludes |
| free words | The subject, the text, the HTML, the addresses, the URL, the event and the body |
The filters next to the box offer every channel, action, provider and check the inbox holds. The search, the filters and the open message are kept in the address, so a link to the inbox opens the same view. The same search works on the API, as GET /v1/messages?q=…; see API.
Echo keeps messages in memory by default (the newest 10,000) and forgets them when it stops. --store keeps them on disk instead:
rhythm echo up --store ~/echo-inbox --keep-for 14drhythm echo 1.1.1 - this machine
Mode: seal: nothing leaves; Echo answers for every service
API: http://127.0.0.1:18025 (connectors post here; open it for the live view)
Inbox: /home/you/echo-inbox (kept across restarts; 0 messages in it now, each kept 14d)
Waiting for messages. Ctrl+C to stop.
Stopped. Captured no messages this session.
(This run used --listen 127.0.0.1:18025 --smtp off.) The same in .echo/config.yaml, relative to the project folder:
inbox: {store: .echo/inbox, keep: 50000, keep_for: 14d}| Flag | Config | Default | Does |
|---|---|---|---|
--store <dir> |
inbox: store: |
memory | The folder to keep the inbox in. |
--keep <n> |
inbox: keep: |
10000 |
The most messages kept, in memory or on disk. Never more than 256 MiB of them; the oldest go first. |
--keep-for <d> |
inbox: keep_for: |
forever | With a store: drop messages older than this, such as 14d or 12h. |
The flags win over the config. --keep-for without a folder is refused: rhythm echo: --keep-for and keep_for: need a folder to keep messages in: --store <dir> or inbox: store: in .echo/config.yaml (exit 2). The folder is made owner-only (0700), with files 0600. A folder that holds files that aren't Echo's is refused, so Echo never deletes anything of yours there. One-time codes and links that Echo reads from messages (for latest) are never written to disk.
Two Echos can't share a folder. The second one exits 3:
rhythm echo hub: open inbox: another Echo (PID 1482) keeps its inbox in /home/you/hub/store/inbox; stop it first, or give this one a folder of its own
Stopped. Captured … this session. counts only what this session caught, not what the folder held before.
Without a token, Echo Inbox answers only to localhost, 127.0.0.1 and other loopback names (and, with --private-network, to IP addresses and service names in a container network; see Docker and DDEV). A page on another site can't read it or frame it: the inbox sends Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self'; frame-src 'self'; script-src 'self'; frame-ancestors 'none' and X-Frame-Options: DENY. Requests carrying another site's Origin are refused.
With --token (or RHYTHM_ECHO_TOKEN), every route needs the token, the inbox too. A browser opens /?token=<your token> once: Echo sets an echo_token cookie (HttpOnly, SameSite=Strict) and sends the browser on to / without the token in the address. The banner says so:
API: http://127.0.0.1:18025 (connectors post here; open /?token=<your token> once for the live view)
curl -s -o /dev/null -D - "http://127.0.0.1:18025/?token=$RHYTHM_ECHO_TOKEN"HTTP/1.1 303 See Other
Location: /
Set-Cookie: echo_token=…; Path=/; HttpOnly; SameSite=Strict
Without it, a request gets 401 with missing or wrong X-Echo-Token (in a browser, open /?token=<your token> once).
--login user:password (better: RHYTHM_ECHO_LOGIN=user:password, since other users can read a flag in ps) puts Echo Inbox behind a sign-in page at /login, for someone who should read what the site sent but not change it, such as a client signing off a launch. The password needs at least 8 characters.
RHYTHM_ECHO_LOGIN=client:correct-horse rhythm echo upLogin: viewers sign in as client at http://127.0.0.1:18025/login (read only, for 8 hours); the API stays open to programs on this machine
A session lasts 8 hours. A signed-in viewer can read the inbox but not change it: deleting answers 403 with signed in to view only: changing the inbox needs Echo's token. The session never opens the API (/v1/…, /api/v1/…), which stays with the token. After 20 wrong passwords in a minute (from all clients together), Echo Inbox refuses every sign-in for up to a minute, so a guesser can lock real viewers out for that minute.
A viewer login beyond loopback needs a token too, so connectors use the token and viewers the login:
rhythm echo: RHYTHM_ECHO_LOGIN needs a token when --listen 0.0.0.0:18025 is reachable from other machines: without one the API answers anyone there; set --token (or RHYTHM_ECHO_TOKEN) for connectors and keep the login for viewers
With a login set, Echo Inbox no longer insists on a loopback Host, so it works behind a reverse proxy that adds TLS. Put the proxy in front for anyone outside your network: the token and the session cross the network in clear otherwise.
Sealed, no email leaves. Sometimes one should: the client wants to see the real order confirmation in their own inbox before launch. A relay lets Echo send one caught email through your own SMTP account, only to the addresses you allow. Each release is logged in the inbox.
# .echo/config.yaml
relay:
allow: ["*@client.example"]export RHYTHM_ECHO_RELAY=smtp://user:pass@smtp.agency.example:587 # keep the password out of the config
export RHYTHM_ECHO_TOKEN=$(openssl rand -hex 16)
RHYTHM_ECHO_LOGIN=client:correct-horse rhythm echo uprhythm echo 1.1.1 - this machine
Mode: seal: nothing leaves; Echo answers for every service
API: http://127.0.0.1:18025 (connectors post here; open /?token=<your token> once for the live view)
SMTP: 127.0.0.1:11025 (point a mailer or sendmail_path here)
Login: viewers sign in as client at http://127.0.0.1:18025/login (read only, for 8 hours)
Relay: releases go out through smtp.agency.example:2587, only to *@client.example (Release in Echo Inbox, or rhythm echo release <id> --to <address>)
Waiting for messages. Ctrl+C to stop.
(This run used a local test SMTP server on port 2587.) An email's detail then has a Release to field and a Release button, which a signed-in viewer may use too. From the command line:
rhythm echo release 2a5397874054 --to ann@client.exampleReleased 2a5397874054 to ann@client.example (logged as 20a3da3c9442).
An address the relay doesn't allow is refused and nothing is sent (exit 2):
rhythm echo: x@evil.test isn't on the relay's allowlist; nothing was sent
Every attempt, sent or refused, is logged in the inbox as its own entry, outside any action, so it never counts in a comparison:
2a5397874054 14:33:12 email to ann@client.example: Your Bean & Leaf order #1042 [faked]
20a3da3c9442 14:33:12 request to ann@client.example: Released: Your Bean & Leaf order #1042 [sent]
c43871858db9 14:33:12 request to x@evil.test: Released: Your Bean & Leaf order #1042 [blocked]
| Rule | Why |
|---|---|
The account comes from RHYTHM_ECHO_RELAY (or relay: url:), as smtp://user:pass@host:587 (STARTTLS) or smtps://user:pass@host:465 (TLS). |
It holds a password: the environment is better than a committed file. |
| Echo never sends a login or a message in clear: a server without STARTTLS is refused. | The account's password and the client's email stay private. |
Only addresses matching allow get a release; an empty list allows none. |
A release can't be turned into a way to mail anyone. |
Releasing needs Echo's token or a viewer signed in with --login. |
up refuses to start a relay without one of them. |
Bcc lines are dropped; the message gets X-Echo-Release: <id>. |
The client gets only their copy. |
Errors at start (exit 2) say what's missing: relay: needs the SMTP account to send through: set RHYTHM_ECHO_RELAY to smtp://user:pass@host:587 (or url: under relay: in .echo/config.yaml), relay: releasing needs Echo's token or a viewer login; set --token (or RHYTHM_ECHO_TOKEN) or --login and relay: allow: lists no address, so nothing could be released; add the client's, e.g. "*@client.example". When the account refuses the message: rhythm echo: the relay at <host:port> didn't take it: <reason> (exit 3). The relay runs on rhythm echo up, not on the agency hub.
Echo answers Mailpit's API too (/api/v1/messages, /api/v1/message/{ID}, /api/v1/search and more), so tools and tests written against Mailpit read Echo Inbox's emails unchanged. See API.
← Sealing staging · Home · Messages and checks →
rhythm · Useful ideas. Working systems. · Check only sites you own or have permission to test. Keep staging sealed (responsible use) · This wiki is published from wiki/ in the rhythm repository; suggest changes there with a pull request.
Get started
rhythm in depth
Tuner
- Overview
- Your first scan
- Reading results
- Choosing checks
- Requests and politeness
- Output and reports
- Batch scans
- Local scans
- Integrity and malware
- Changes and notifications
Tuner in depth
- Known vulnerabilities
- PHP advisories
- CI and code scanning
- GitHub Action
- Config file
- Watch and serve
- Writing checks
- Check packs
- Finding reference
- CLI reference
- Coming from Wordfence CLI or WPScan
- FAQ and troubleshooting
Soundcheck
- Overview
- Your first check
- Reading results
- Review and accept
- Discovery
- Page health
- Visual changes
- Interactions
- Forms and flows
- Cache
- Speed
- Accessibility and SEO
Soundcheck in depth
- CI
- CI baselines and PR comments
- Configuration
- Error rules and exec
- WordPress updates
- Fleet and client reports
- History
- Import from BackstopJS and Lost Pixel
- Firefox and WebKit
- CLI reference
- FAQ and troubleshooting
- Benchmark
Echo
- Overview
- Your first run
- Connectors
- Sealing staging
- Echo Inbox
- Messages and checks
- Checks and rules
- Attachments
- Actions and compare
- With Soundcheck
- Replay
- Deliverability
Echo in depth
- Docker and DDEV
- Agency hub and reports
- Schedule
- MCP server
- Conformance kit
- CI
- Configuration
- API
- CLI reference
- FAQ and troubleshooting
- Benchmark
Project