Skip to content

0.24.0: mail, the inbox from the terminal for two accounts - #48

Merged
ralyodio merged 1 commit into
masterfrom
feat/mail
Sep 5, 2026
Merged

0.24.0: mail, the inbox from the terminal for two accounts#48
ralyodio merged 1 commit into
masterfrom
feat/mail

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

What

A mail command: read, search, reply, send, file and delete over IMAP, sending through the account's SMTP with Resend as the fallback for a domain verified there. Accounts are configuration, not code: ~/.config/cli-tools/mail.json (0600), imported from the new cli-tools-mail--prod team vault as MAIL_<NAME>_EMAIL / _PROVIDER / _PASSWORD plus MAIL_DEFAULT. Nothing in the tree names a person.

  • mail login <provider|address> [address] [--as NAME] — sign in to any provider that still takes a password. Says which kind of password the host wants before asking (app password, per-alias password, bridge password, or the account one), tries IMAP and SMTP, and stores nothing on a refusal. The provider is read off the name, a webmail domain, or a custom domain's MX records.
  • mail providers — the fifteen built in (Forward Email, Gmail / Workspace, Yahoo, AOL, iCloud, Fastmail, Zoho, Proton via Bridge, GMX, Yandex, mail.com, Posteo, mailbox.org, Migadu, Purelymail) with hosts, ports and the password rule, and the ones a password cannot reach (Outlook.com / Microsoft 365: OAuth2 only; Tuta, HEY: no IMAP) with the reason.
  • mail accounts [add|password|default|rm|pull]
  • mail folders, mail ls [-a all] [--unread], mail search <query>, mail read <uid>
  • mail send, mail reply <uid> [--all], --draft files to Drafts instead
  • mail mark, mail mv, mail archive, mail rm [--purge]
  • Plugin mail (/mail:login, /mail:inbox, /mail:send) in the marketplace; README section; resend joins KNOWN_KEYS.
  • promptSecret and promptLine in src/prompt.ts, shared by both commands.

Decisions worth a look

  • The kind of password is announced before it is asked for. Gmail, Yahoo, AOL, iCloud, Fastmail and Yandex refuse the account password over IMAP on purpose, and the refusal reads like a typo. Every login failure now names the provider's fix, not only Gmail's.
  • Nothing is stored on an IMAP refusal. A stored password that does not work makes every later command fail further from the cause. An SMTP-only refusal stores the account with a warning, because reading still works. --no-verify opts out.
  • MX records identify a hosted custom domain. mail login you@yourdomain.com resolves the domain's MX and matches the host (Google, Zoho, Fastmail, Proton, iCloud, Forward Email, Migadu, …); a Microsoft-hosted domain gets the OAuth2 explanation. A failed lookup is simply "say which provider".
  • Outlook is listed, not attempted. Microsoft removed basic auth (Microsoft 365 IMAP in 2022, personal accounts 2024-09-16) and app passwords no longer count, so mail login outlook explains instead of failing a login.
  • Proton is Bridge-only. Localhost STARTTLS on 1143/1025 with a self-signed certificate; accounts gain imapSecure and insecureTls for exactly that, and insecureTls is never set for a real host.
  • Resend never carries a webmail address. Every provider's domains count as webmail, so those accounts are SMTP-only and the error says which fix applies.
  • Fallback only on a pipe failure. A refused login or dead host falls back to Resend; a refused message does not.
  • Reading marks read explicitly. imapflow fetches with PEEK, so mail read sets \Seen itself; --keep-unread skips that.
  • A Resend send is appended to Sent over IMAP, because Resend files nothing.

Verified

  • 673 tests pass (69 in test/mail.test.ts); typecheck clean apart from the two pre-existing free-names errors on master.
  • Live against the business account (Forward Email), 2026-09-05: mail login anthony@… --as worktest with no provider named read Forward Email off the MX records, verified both IMAP and SMTP, stored the account, mail ls -a worktest listed the inbox, accounts rm removed it. accounts, folders, ls, search, read, send --draft, rm --purge all work with the real password. MAIL_WORK_PASSWORD is in the cli-tools-mail--prod vault, so a fresh box needs only mail accounts pull.
  • mail login outlook …, mail login me@hotmail.com, an unknown provider name, a bare address on an unknown domain, and a wrong Gmail password each produce the intended message and store nothing.
  • mail accounts pull from the vault on the dev box, end to end; a self-addressed message sent through Resend with the vault key.
  • The personal Gmail account still needs an App Password (mail login gmail … --as personal).

🤖 Generated with Claude Code

https://claude.ai/code/session_013GX6xEcjGFxcvcMCFLhQng
https://claude.ai/code/session_013h8jopY81BGQ4Pn22NfZTu

Read, search, reply, send, file and delete over IMAP, sending through the
account's SMTP with Resend as the fallback for a domain verified there.
Accounts are configuration (~/.config/cli-tools/mail.json, 0600) and import
from the cli-tools-mail team vault as MAIL_<NAME>_EMAIL/_PROVIDER/_PASSWORD;
nothing in the tree names a person. A webmail address never goes through
Resend, because its domain cannot be verified there. Replies thread
(In-Reply-To, References, Reply-To honoured) and quote the original.

Also lifts promptSecret out of the dispatcher into src/prompt.ts so both
commands share it, and adds resend to KNOWN_KEYS.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GX6xEcjGFxcvcMCFLhQng
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​types/​mailparser@​3.4.61001007380100
Added@​types/​nodemailer@​8.0.11001007687100
Addedmailparser@​3.9.20991008095100
Addednodemailer@​10.0.0981009896100
Addedimapflow@​1.7.8991009896100

View full report

@socket-security

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
License policy violation: npm @zone-eu/mailsplit under EUPL-1.2

License: EUPL-1.2 - The applicable license policy does not permit this license (5) (package/LICENSE.EUPL-1.2)

License: unrecognized license - This license was not allowed or given any lesser classification by the applicable policy (package/LICENSE.EUPL-1.2)

License: EUPL-1.1+ - This license classifier is not allowed by the applicable policy (package/package.json)

From: pnpm-lock.yamlnpm/imapflow@1.7.8npm/mailparser@3.9.20npm/@zone-eu/mailsplit@5.4.16

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@zone-eu/mailsplit@5.4.16. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

Comment thread src/mail.ts
export function parseQuery(input: string): SearchObject {
const query: SearchObject = {};
const text: string[] = [];
const tokens = input.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [];
Comment thread test/mail.test.ts
const resolved = resolveAccount(
'work',
{ email: 'a@example.com', provider: 'forwardemail', password: 'stored' },
env({ MAIL_WORK_PASSWORD: 'exported' }),
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

10 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 4 | LOW: 4

Severity Rule Location
HIGH sh-remote-script-execution root-ubuntu.sh:2816
HIGH sh-remote-script-execution root-ubuntu.sh:2820
MEDIUM sh-remote-script-execution root-ubuntu.sh:2969
MEDIUM sh-remote-script-execution root-ubuntu.sh:3998
MEDIUM redos-nested-quantifier src/domain-free.ts:56
MEDIUM redos-nested-quantifier src/mail.ts:615
LOW secret-generic-credential src/credentials.ts:36
LOW secret-generic-api-key test/credentials.test.ts:208
LOW secret-generic-credential test/mail.test.ts:123
LOW secret-generic-credential test/shorten.test.ts:36

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio marked this pull request as ready for review September 5, 2026 22:59
@ralyodio
ralyodio merged commit 3cfc5a9 into master Sep 5, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants