Submail Client Skill
Submail is a virtual inbox router. Each agent has a dedicated email address and
API token. This skill covers everything needed to configure and use the
submail CLI to access that inbox.
Prerequisites
submailbinary is on$PATH- You know your server URL, agent token, and (optionally) your profile name
Step 1 — Configure a profile (one-time setup)
Profiles store the server URL and token so you never pass them as flags.
Profile files live at ~/.config/submail/profiles/<name>.yaml.
submail profile set <profile-name> \
--url <server-url> \
--token <your-agent-token>
Example:
submail profile set hawkeye \
--url http://localhost:8080 \
--token hawkeye-secret-token
Validate the profile was saved correctly
submail profile show <profile-name> --json
Expected output (token is always masked):
{"name":"hawkeye","token_set":true,"url":"http://localhost:8080"}
Check: token_set must be true and url must be non-empty. If either is
wrong, re-run profile set with the correct values.
Set the active profile for this agent session
export SUBMAIL_PROFILE=<profile-name>
After this, all submail inbox commands use the profile automatically — no
--url or --token flags needed.
Step 2 — Verify connectivity
Run a quick sanity check before doing real work:
submail inbox list --json --limit 1
| Exit code | Meaning | Fix |
|---|---|---|
0 |
Connected and authenticated | — |
4 |
Token rejected | Re-run profile set with the correct token |
1 |
Server unreachable | Check url in the profile; confirm server is running |
A successful response looks like:
{"mails":[...],"total":5,"limit":1,"offset":0}
If total is 0 the inbox is empty — that is not an error.
Available tasks
List messages
submail inbox list --json
Response schema:
{
"mails": [
{
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"message_id": "<envelope-message-id@sender.example>",
"subject": "Subject line",
"from": "sender@example.com",
"to": "bot+agentname@example.com",
"received_at": "2026-01-01T00:00:00Z"
}
],
"total": 42,
"limit": 50,
"offset": 0
}
Key fields:
id— UUID used to fetch the full message; use this ininbox getreceived_at— ISO 8601 UTC timestamptotal— total matching messages (ignoreslimit/offset)mailsmay be an empty array[]— that is not an error
Pagination
# First page
submail inbox list --json --limit 20 --offset 0
# Second page
submail inbox list --json --limit 20 --offset 20
Iterate until offset + len(mails) >= total.
Get only IDs (pipe-friendly)
submail inbox list --quiet
Outputs one UUID per line. Useful for looping or piping.
Get a single message
submail inbox get --json <id>
<id> must be a lowercase UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
Obtain it from the id field in inbox list.
Response schema (same as list items, plus body fields):
{
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"message_id": "<envelope-message-id@sender.example>",
"subject": "Subject line",
"from": "sender@example.com",
"to": "bot+agentname@example.com",
"received_at": "2026-01-01T00:00:00Z",
"text_body": "Plain-text content of the email.",
"html_body": "<html>...</html>"
}
text_body— prefer this for content parsing; may be empty for HTML-only emailshtml_body— raw HTML; present when the sender included an HTML part- Both fields are omitted (not
null) when absent
Exit codes
| Code | Meaning | Retryable |
|---|---|---|
0 |
Success | — |
2 |
Bad input (e.g. malformed UUID) | No — fix the input |
3 |
Message not found | No |
4 |
Unauthorized — wrong token | No — fix the profile |
1 |
Server / network error | Yes |
Always check $? after each command. On non-zero exit (JSON mode), stdout
contains a structured error:
{
"error": "not_found",
"message": "mail not found",
"input": {"id": "00000000-0000-0000-0000-000000000000"},
"retryable": false
}
Use retryable to decide whether to retry or abort.
Common workflows
Read the most recent message
ID=$(submail inbox list --quiet --limit 1)
submail inbox get --json "$ID"
Process all messages newest-first
LIMIT=20
OFFSET=0
while true; do
PAGE=$(submail inbox list --json --limit $LIMIT --offset $OFFSET)
TOTAL=$(echo "$PAGE" | jq '.total')
IDS=$(echo "$PAGE" | jq -r '.mails[].id')
for ID in $IDS; do
submail inbox get --json "$ID" | jq '.text_body'
done
OFFSET=$((OFFSET + LIMIT))
[ $OFFSET -ge $TOTAL ] && break
done
Check for new mail since a known timestamp
inbox list does not have a since filter — fetch the list and filter
client-side by received_at:
submail inbox list --json --limit 50 | \
jq '[.mails[] | select(.received_at > "2026-01-01T00:00:00Z")]'
Profile reference
| Command | Purpose |
|---|---|
submail profile set <name> --url URL --token TOKEN |
Create or update a profile |
submail profile show <name> --json |
Verify a profile (token masked) |
submail profile list --json |
List all configured profiles |
submail profile delete <name> |
Remove a profile |
Profile files are stored at ~/.config/submail/profiles/<name>.yaml with
mode 0600 (readable only by the current OS user).
Credential precedence (most specific wins)
--url/--tokenflagsSUBMAIL_URL/SUBMAIL_TOKENenvironment variables--profileflag orSUBMAIL_PROFILEenvironment variable~/.config/submail/profiles/default.yaml(silent fallback)