A modern client for Zoho Mail.
Zoho disables standard IMAP/SMTP/POP3 on the free plan, pushing you toward a paid plan just to use your own mail client. Finch uses the official Zoho Mail REST API via OAuth 2.0 instead, so you get a real inbox - read, send, folders, attachments, the works - without paying for premium protocol access.
Sign-in is Zoho OAuth only. There's no separate username/password account system: your identity is your Zoho mailbox, so signing in and connecting your inbox happen in one step.
| Sign in | Bring your own Zoho app | Inbox |
|---|---|---|
![]() |
![]() |
![]() |
- Node.js installed on your system.
npm install
npm startOpen http://localhost:3000, click Use my own Zoho API credentials, and register
a free app on the Zoho Developer Console:
- Add Client -> Server-based Applications
- Client Name: anything, e.g.
My Mail Client - Homepage URL: the URL you're running this at (e.g.
http://localhost:3000) - Authorized Redirect URIs: exactly what's shown on the sign-in screen
(e.g.
http://localhost:3000/oauth2callback)
Paste the resulting Client ID and Client Secret in, submit, and you'll be redirected to Zoho to authorize - land back in your inbox once you accept.
Your credentials are encrypted at rest and tied to the account that was created for your Zoho email address, so multiple people can each bring their own Zoho app to the same running instance.
If you're running this for other people, register one Zoho app yourself and set it via environment variables - every visitor then gets a one-click sign-in button instead of the manual credential form above.
Same registration steps as above, using the same /oauth2callback redirect URI.
Set:
ZOHO_CLIENT_ID=1000.XXXXXXXXXXXXXXXXXXXXXXXX
ZOHO_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ZOHO_REGION=us # us | eu | in | au | cn | jpPeople can still use Use my own Zoho API credentials even when this is set, if they'd rather bring their own app.
| Variable | Default | Purpose |
|---|---|---|
PORT |
3000 |
Port the server listens on. |
BASE_URL |
derived from the incoming request | Set explicitly for a remote deployment behind a fixed domain, e.g. https://mail.example.com. Must match whatever redirect URI you register with Zoho. |
ENCRYPTION_KEY |
auto-generated into data/.encryption-key on first run |
A 64-character hex string (32 bytes) used to encrypt stored Zoho client secrets and OAuth tokens at rest (AES-256-GCM), and to encrypt the short-lived pending-credentials cookie used during bring-your-own sign-in. Set this explicitly (and back it up) for a real deployment - losing it means every account's Zoho connection needs to be reconnected. |
BRAND_NAME |
Finch |
Shown as the page title. |
BRAND_LOGO_URL |
/logo.png |
Used as the favicon and sidebar logo. Point it at your own image (a path under public/, or any external URL). |
BRAND_ACCENT_COLOR |
none (falls back to the theme default) | A hex color (e.g. #4f7cff) used as the default accent color. Never overrides a color a visitor has personally picked in Settings. |
All account data lives in the gitignored data/ directory (users.json,
sessions.json, .encryption-key) - back that directory up if you don't want
to reconnect everyone's Zoho Mail later.
The app name, logo, and accent color are all configurable via the environment
variables above - no code changes needed. Set them, and drop your logo file
under public/ (or point BRAND_LOGO_URL at an external image instead). If
you don't want your custom logo public, keep the file out of version control
and reference it through BRAND_LOGO_URL rather than committing it.
- Zoho client secrets, access tokens, and refresh tokens are encrypted at rest
with AES-256-GCM (
lib/crypto-secrets.js), keyed byENCRYPTION_KEY. - Passwords don't exist in this system - there's nothing to hash, salt, or leak.
- Sessions are opaque random tokens (server-side lookup only), stored in an
httpOnly,SameSite=Lax,Secure(when served over HTTPS) cookie. - The bring-your-own-credentials flow briefly carries your Client ID/Secret
through the OAuth redirect in an
httpOnlycookie - but the cookie's value is itself AES-256-GCM ciphertext, so the browser never sees the plaintext, and any tampering fails the GCM auth tag check. It expires after 10 minutes. - Incoming email content (sender name, subject, snippet) is untrusted by definition - anyone who emails you controls it - so it's HTML-escaped before being rendered in the message list, rather than trusted as safe markup.
The client requests the minimum required permissions to read and send emails:
ZohoMail.accounts.READ- Get your account ID and email address details.ZohoMail.folders.READ- Fetch your email folders list (Inbox, Sent, Drafts, etc.).ZohoMail.messages.ALL- Read emails, fetch email bodies, and compose/send emails.
MIT


