@danj/hey-email
HEY.com email for Kody: search threads, read messages, download PDF attachments, and write drafts. Sending is barred until you allow it.
README.md
237 lines · 12.9 KB · Markdown@danj/hey-email
Intent
Let your agents, and the Kody packages they build, search, read, and draft
your HEY mail. The package calls HEY at app.hey.com, through the same API
that HEY's CLI and SDK use. Kody never runs the hey CLI. You run it once on
your computer, to sign in for Kody.
Sending is barred until you allow it. SENDING in src/config.js is the
switch. While it is 'barred', no function sends mail, and your agent saves
drafts for you to send. The section "The sending switch" explains how to
allow sending.
The package is publish-locked on Kody. A change goes live only after you review it and click Promote this commit.
What you can ask your agent
Ask your agent for any of these:
- "Find the invoice from Acme last week and read me its total from the PDF."
- "Summarize my thread with Ada."
- "Draft a reply to Ada that says the Tuesday slot works." The agent saves a reply draft and gives you its edit link.
- "Email Bob that I'm running late." The agent saves a new draft addressed to Bob and gives you its edit link.
- "Move this Logos newsletter thread to Paper Trail." The agent moves that one thread. Other mail from the same sender stays where it is.
You do these yourself:
- Send each draft. Open its edit link in HEY, check the text and the recipients, and send.
- Sign in to HEY, and paste your secrets on Kody's setup pages.
- Review and promote each change to the package on Kody.
- Decide whether sending is ever allowed.
What it does
Each ./name export imports as kody:@danj/hey-email/name.
| Export | What it does | HEY calls |
|---|---|---|
kody:@danj/hey-email | Returns your HEY { id, name }. Use it as a smoke test. | GET /identity |
./search | Searches your mail. Pass limit greater than 10 to fetch further pages. | GET /advanced_search.json, then ?page=2 and so on |
./paper-trail | Moves one thread to the Paper Trail, by topic id or entry id. | GET /boxes.json, then POST /topics/{id}/moves.json |
./boxes | Lists your boxes, such as the Imbox and the Feed. | GET /boxes.json |
./topic | Reads one thread. | GET /topics/{id}.json |
./entries | Lists the messages in a thread. | GET /topics/{id}/entries.json |
./message | Reads one message with its HTML body. | GET /messages/{id}.json |
./attachments | Lists PDF attachments on a message, then downloads one file as base64. The default export is listPdfs: metadata only, no bytes. | GET /messages/{id}.json to list. One GET for a chosen PDF. |
./drafts | Lists, reads, and saves drafts with their recipients. HEY keeps each saved draft as a draft. It also exports the three send functions. | GET /entries/drafts.json, GET /messages/{id}/edit.json, POST /messages.json, POST /entries/{id}/replies.json |
./reply | Saves a reply draft on a thread. | GET /entries/{id}/replies/new.json, POST /entries/{id}/replies.json |
./send | sendMessage, sendReply, and sendDraft. | None while sending is barred. While it is allowed, a confirmed send calls POST /messages.json, POST /entries/{id}/replies.json, or PUT /messages/{id}.json. |
AGENTS.md lists each function's inputs, results, and errors.
The default export of ./attachments is listPdfs. It returns metadata only
(index, filename, contentType, url, filesize) and has no bytes.
There is no combined download of every file. Call downloadPdf for one PDF
as base64.
Set it up
You do the steps that need your HEY sign-in, your secrets, or your review. Your agent does the rest. Your secret values never pass through the agent.
You need a HEY account. HEY is a paid 37signals product with a free trial at hey.com. A
@hey.comaddress and a HEY for Domains address both work.You install the HEY CLI on your computer, version 1.4.0 or later. Follow the hey-cli install guide.
You sign in to HEY for Kody, in a new folder. To HEY, Kody is then a separate device, like a second computer. Your own CLI login stays as it is. Run these lines in one terminal window, and keep that window for steps 5 and 7.
HEY_KODY=$(mktemp -d) XDG_CONFIG_HOME="$HEY_KODY" HEY_NO_KEYRING=1 hey auth login XDG_CONFIG_HOME="$HEY_KODY" HEY_NO_KEYRING=1 hey auth statushey auth loginopens HEY's sign-in page in your browser.HEY_NO_KEYRINGmakes the CLI save this login in the folder, not in your keychain. In the status, theAuth:line must showoauth, and theStorage:line must showfile. TheToken:line shows the token cut short with..., so do not copy it from there. Step 5 copies the full values. Do not add--cookieor--tokento the login. Those logins store no refresh token, so this package cannot use them. HEY can email you about a sign-in from a new device.Your agent sends you three Kody setup links, one for each secret. Each link fills in the secret's name, its description, and the allowed host
app.hey.com. TheheyTokenandheyRefreshTokenlinks also ask you to let this package update them, so it can save new tokens after a refresh.You copy each value from the folder and paste it on its link. Each command copies one value to the clipboard. The first two need
jq, which macOS 15 and later include.heyToken:jq -j '."https://app.hey.com".access_token' "$HEY_KODY/hey-cli/credentials.json" | pbcopyheyRefreshToken:jq -j '."https://app.hey.com".refresh_token' "$HEY_KODY/hey-cli/credentials.json" | pbcopyheyInstallId:tr -d '\n' < "$HEY_KODY/hey-cli/install_id" | pbcopy
After the last paste, clear the clipboard with
pbcopy </dev/null. On Linux, replacepbcopywithwl-copyorxclip -selection clipboard, and clear the clipboard withwl-copy --clear. If you use a clipboard manager, delete the three entries from its history. Tell your agent when all three secrets are saved.Your agent runs
identity(). It must return your HEY id and name.You delete the folder with
rm -rf "${HEY_KODY:?}". Kody now holds the only copy of this login. Nothing else refreshes its tokens, so Kody's copy stays valid.Your agent locks the package on Kody, if it is not locked yet. From then on, each change waits on the package page until you click Promote this commit. Only you can unlock it, on the package's settings page.
Use it from another package
The agent that builds your package follows these steps.
- Add
"@danj/hey-email": "*"tokody.dependencies, and import the exports askody:@danj/hey-email/<export>. In a fork of this package, use your own scope instead of@danj. - Read
AGENTS.mdfor each function's inputs, results, and errors. - Your package bundles a snapshot of this one. After each change here, republish your package.
The sending switch
SENDING in src/config.js is the switch. Its committed value is
'barred'. Only you decide to change it.
SENDING = 'barred' (committed) | SENDING = 'allowed' | |
|---|---|---|
sendMessage, sendReply, sendDraft | Return the barred result, with barred: true and sent: false. No request reaches HEY. | Send when confirm is exactly true. Otherwise, return a dry run, and send nothing. |
createDraft, createReplyDraft | Save a draft with its recipients. HEY does not send it. | Same. |
An agent must not change SENDING to finish an email request. While sending
is allowed, an agent still saves a draft, unless you ask it in plain words to
send now. An agent must not send from an unattended run, such as a reminder
job. The code does not enforce these two rules, so the lock on Kody matters.
Allowing sending
- You change
SENDINGto'allowed'. You can commit to main, or open a pull request. You can also ask your agent in plain words to make this change. - Three test files and the test hook pin the barred value. The same commit
updates
test/sending-gate.test.js,test/sending-allowed.test.js,test/redirects.test.js, andtest/hooks.mjs. It also updates the two package descriptions inpackage.json. - The
sending-gatecheck fails on a branch or pull request that sets'allowed'while main is still'barred'. So a pull request with this change shows a failed check. You can still merge it. After that, later branches carry'allowed'from main, and the check passes them. - Your agent publishes the package to Kody. You review the change on the package page, and click Promote this commit. Then your agent republishes the packages that use this one.
Security posture
| Secret | What it holds | The package sends it to | The package updates it |
|---|---|---|---|
heyToken | The access token. It expires after about 14 days. | app.hey.com | After each refresh |
heyRefreshToken | The refresh token | app.hey.com/oauth/tokens | When HEY returns a new one |
heyInstallId | The install id of Kody's login. HEY ties the refresh token to it. | app.hey.com/oauth/tokens | Never |
The package sends {{secret:...}} placeholders. Kody fills them in only for
the host app.hey.com, on any path.
What this design guarantees:
- Credentials go only to
app.hey.com.resolveHeyUrlrefuses every other origin. - The package follows redirects itself. A redirect off
app.hey.comdrops the token. PDF downloads continue to the file storage host as a plainGET. JSON calls refuse the redirect. Token refresh refuses every redirect. - On a
HEY 401fromapp.hey.com, the package refreshes the token once and retries. A 401 from the PDF storage host does not start a refresh. Before the package spends the refresh token, it checks that it can save the new pair. It saves both tokens in one batch, and retries the save once. - No function sends mail while
SENDINGis'barred'. - The package is publish-locked. No change, including a change to
SENDING, goes live on Kody before you promote it.
What it leaves open:
- Any code that imports this package can read all your mail and save
drafts. While
SENDINGis'allowed', that code can also send mail. - During a refresh, the package code holds the new tokens in memory until Kody saves them.
- GitHub refuses branch protection on this private repository on the free
plan. So the
sending-gatecheck does not block a merge or a push to main. The lock on Kody is the control that holds.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
HEY 401 <path> | HEY refused the new token after a successful refresh. | Run setup steps 3 to 7 again. |
HEY token refresh failed (HTTP <n>) | HEY refused the refresh token. A wrong heyInstallId, or a token from a --cookie or --token login, gives the same error. | Run setup steps 3 to 7 again. |
HEY token refresh aborted: this package cannot write heyToken / heyRefreshToken | The package has no grant to update the two secrets. | Ask your agent for the approval link. |
Error code HEY_TOKEN_ROTATION_UNSAVED | HEY issued new tokens, but Kody did not save them. The saved refresh token no longer works. | Run setup steps 3 to 7 again. |
Error code HEY_TOKEN_PERSIST_UNSUPPORTED | The Kody runtime lacks kody.secretSetMany. | Report it to Kody. |
Refusing to send HEY credentials to <origin> | A URL outside app.hey.com reached the package, such as a PDF link in an email. downloadPdf stops there and returns no file. | Open the file in HEY. |
Refusing to follow HEY redirect to <origin> | HEY redirected a JSON call off app.hey.com. | None. The package refuses it. |
| Kody says the host is not approved | app.hey.com is not an allowed host of the secret. | Ask your agent for the host approval link. |
listPdfs returns no PDFs | The HTML came from the entries list, which omits attachments. | Pass the message id. |
Development
The package has no dependencies. npm test runs the Node test runner. The
test hooks in test/hooks.mjs stub kody:runtime, and they load a second
copy of src/ where SENDING is 'allowed'. The committed file stays
'barred'.
The sending-gate GitHub Actions workflow runs on Node 22, on pull requests
and on pushes to every branch except main. It runs
scripts/check-sending-policy.mjs, then npm test. The script fails when
the branch sets SENDING to 'allowed' and origin/main does not.
For your own terminal, use the HEY CLI directly. It also ships an agent skill and an MCP server, described in the hey-cli agent guide.
Credits and license
This package is a fork of @noah/hey-email on Kody, by Noah (HiThereHey).
Noah wrote the first HEY API wrapper, the draft and send functions, and the
token refresh. Daniel Jomphe's fork adds the redirect rules, safer token
saving, the sending switch, the HEY SDK request format, and the separate
Kody login.
MIT. LICENSE keeps Noah's copyright notice, and adds Daniel Jomphe's for
the fork's changes.