Tresso is an open source email client built around a single idea: every email is something to do, to hand over, or to know.
Repo: github.com/gettresso/tresso
Install Β· How it works Β· Highlights Β· Your own server Β· Development Β· Releases
The desktop app runs on macOS, Linux and Windows. It signs in to Tresso Cloud, or to your own server.
curl -fsSL https://sherin.dev/tresso/install.sh | bashPin a release with bash -s:
curl -fsSL https://sherin.dev/tresso/install.sh | bash -s v0.1.0On macOS the script puts Tresso.app in /Applications (or ~/Applications when that is not writable). On Linux it installs the AppImage in ~/.local/share/tresso, links tresso into ~/.local/bin, and adds a launcher entry that also opens tresso:// links, through which sign-in comes back. TRESSO_VERSION pins a release like the first argument, and TRESSO_INSTALL picks another folder. Run it again to upgrade in place.
powershell -c "irm https://sherin.dev/tresso/install.ps1|iex"The script runs the installer silently for your user: Tresso goes to %LOCALAPPDATA%\Tresso and into the Start menu. $env:TRESSO_VERSION pins a release (v0.1.0). Remove it again under Settings > Apps.
Both scripts check the download against SHA256SUMS on the GitHub release before they install anything.
After that, Tresso keeps itself up to date. On macOS it uses Sparkle: it checks every few hours, and Tresso > Check for Updates⦠asks right away. On Windows and Linux (the AppImage) a card offers the new version. Every update is signed, and the app installs only updates whose signature matches the key built into it. Before it restarts into an update, Tresso saves the draft you are writing and waits for files you are attaching and a message being sent. Settings > Updates shows your version.
Published files
Every release has:
| Platform | File |
|---|---|
| macOS (Apple Silicon and Intel) | tresso-macos-universal.dmg, tresso-macos-universal.app.tar.gz |
| Linux x86_64 | tresso-linux-x86_64.AppImage, tresso-linux-x86_64.deb |
| Linux arm64 | tresso-linux-aarch64.AppImage, tresso-linux-aarch64.deb |
| Windows x64 | tresso-windows-x64-setup.exe, tresso-windows-x64.msi |
Prereleases (v1.2.0-beta.1) ship the Windows setup without the .msi, which only takes numeric versions.
The app is not signed with an Apple Developer ID yet. The install script lifts the macOS quarantine for you. When you open the .dmg yourself, right-click Tresso and choose Open the first time, or run xattr -dr com.apple.quarantine /Applications/Tresso.app. The AppImage needs FUSE 2 (libfuse2, or libfuse2t64 on Ubuntu 24.04 and later); the .deb does not.
| Verdict | What happens | Done when |
|---|---|---|
| Do | It becomes a todo, linked to the email, with a due date when there is one | The todo is completed and the email archives itself |
| Hand over | It goes to someone else with a note and a due date | They finish it; until then it sits in Waiting on |
| Know | It lands in the daily brief | One click on Noted |
- Unified Inbox across all your accounts, grouped into smart inboxes (To do, Informational, Notifications, Newsletters, Invitations, Spam) plus your own, defined by a name, instructions and sender include/exclude lists.
- Built-in todos that sync across devices, can be created automatically from email, and get time in your calendar.
- Triage mode: one email at a time, settled with D, H or K.
- Calendar next to your mail: invitations answered from the email, events from what an email proposes.
- One account, every device: settings, mail accounts and HTML signatures stay in sync.
- Attachment virus scanning before anything can be opened, and your mail encrypted at rest per user.
- Everywhere: web, desktop (Tauri) and mobile (Expo), with notifications only for mail that needs you.
Tresso is fully open source. Everything it does is in this repository, and running it on your own server is free, with every feature. What is still on the roadmap, such as AI with your own key or endpoint, teams with shared inboxes, webhooks and MCP tools for mail, todos and the calendar, lands here too.
Tresso Cloud is the managed hosting of that same code: a subscription pays for running it for you, with backups and support, never for features. See Self-hosted and Tresso Cloud and Billing.
Every release publishes the server as images on GitHub's container registry: ghcr.io/gettresso/tresso-api (the API
and its workers), tresso-sync (the sync workers) and tresso-web (the web client), for amd64 and arm64. Tresso Cloud
runs the same images. To run them, you need a server with Docker, two DNS names pointing at it (one for the web
client, one for the API) and ports 80 and 443 open:
-
Download
compose.prod.yamland.env.prod.exampleinto one folder. -
Copy
.env.prod.exampleto.envand fill it in: your two addresses and every secret, each generated (openssl rand -hex 32). Keep a copy ofDATA_KEYSsomewhere safe: it encrypts the mail at rest, and losing it loses that mail. The API refuses to start while a secret is missing, too short, or one of the development values in this repository, or while an address is not a plainhttps://address. -
Start it:
docker compose -f compose.prod.yaml --env-file .env up -d
-
Create your account:
docker compose -f compose.prod.yaml exec api php bin/console app:user:create you@example.com "Your Name"
The stack gets HTTPS certificates by itself, migrates the database on start and runs PostgreSQL, Redis, object
storage (RustFS), Meilisearch and ClamAV next to Tresso; ClamAV needs about 1.5 GB of RAM. To upgrade, set
TRESSO_VERSION (or keep latest) and run docker compose -f compose.prod.yaml pull before up -d. Two settings
only apply the first time: POSTGRES_PASSWORD (the database keeps its password) and OAUTH_PASSPHRASE (it encrypts
the OAuth signing key; a new passphrase needs a new key: remove both files from the tresso-server_oauth_keys volume
and restart, after which the apps renew their sign-in).
Running the images without this compose file, set the same settings as compose.prod.yaml does. Exactly one API
container (running the image's default frankenphp command) needs RUN_MIGRATIONS=1: on start it makes the OAuth
signing keys, migrates the database and creates the OAuth clients. Every other container, more API containers and the
workers, needs those keys too (mount them read-only at /app/config/jwt). CORS_ALLOW_ORIGIN and
MERCURE_CORS_ORIGINS list the allowed origins separated by spaces: the web address and the desktop app's own,
tauri://localhost and http://tauri.localhost.
In the desktop app, choose Use another server on the sign-in screen and enter the API address, such as
api.mail.example.com. The app checks that the server answers and remembers it until you choose again. The web
client is at your web address.
The API has an MCP endpoint at /mcp, so Claude and other MCP clients can change
your settings for you: signatures and the default signature of each mail account, smart inboxes and their senders,
notifications, batches, the daily brief, reminder rules, calendars and more. A client signs in with your consent and
only gets the permissions you allow; every tool does exactly what the API allows with that permission. Mail, todos and
events are coming next.
Tresso Cloud. In Claude Code:
claude mcp add --transport http tresso https://api.gettresso.com/mcpThen run /mcp in Claude Code and choose Authenticate: your browser opens Tresso's sign-in and a screen that asks
which permissions to give. In Claude.ai (or Claude Desktop), add a custom connector under Settings, Connectors with
the URL https://api.gettresso.com/mcp. On your own server, use your API address instead, such as
https://api.mail.example.com/mcp.
Locally (the development stack below):
claude mcp add --transport http tresso http://localhost:8080/mcpOr skip the sign-in with a personal access token (Settings, Access tokens, with the scopes the tools need):
claude mcp add --transport http tresso http://localhost:8080/mcp --header "Authorization: Bearer tresso_pat_..."Settings, Connected apps lists the apps you allowed and disconnects them. See Authentication and authorization for the tools and the scope each one needs.
| Part | Technology |
|---|---|
| API | PHP 8.5, Symfony 8.1 on FrankenPHP (worker mode), Doctrine, Messenger, OAuth2 server |
| Sync workers | Go 1.27 |
| Web | Vue 3, Vite, TypeScript, Pinia |
| Desktop | Tauri 2 (Rust) wrapping the web app |
| Mobile | Expo (React Native) |
| Data | PostgreSQL 18, Redis, S3 storage (RustFS locally), Meilisearch |
| Scanning | ClamAV |
apps/
api/ Symfony API: OAuth2 server, domain, background jobs
sync/ Go sync workers
web/ Vue 3 web client
desktop/ Tauri 2 shell around apps/web
mobile/ Expo app
website/ Marketing site at gettresso.com (Astro)
packages/
core/ Shared TypeScript: domain types, OAuth2 PKCE client
docker/ Dockerfiles and server config
docs/ Architecture and roadmap
scripts/ Install scripts for the desktop app
assets/ README images
You need Docker, Node 24 or newer, and Rust for the desktop app. Go and PHP run inside containers.
-
Start the stack:
docker compose up -d
On first boot the API installs its dependencies, generates OAuth2 signing keys, runs the migrations and creates the first-party OAuth2 clients (
web,desktop,mobile,sync-worker). -
Create a user (you are asked for a password):
docker compose exec api php bin/console app:user:create you@example.com "Your Name"
-
Install the JavaScript workspaces and start the web app:
npm install
npm run dev:web
Open http://localhost:5173 and sign in.
The desktop and mobile apps start with:
npm run dev:desktopnpm run dev:mobileConnecting Gmail and Microsoft 365 accounts needs an OAuth app at Google and at Microsoft, registered by whoever runs the server. docs/ARCHITECTURE.md lists the steps. In short:
-
Redirect URIs:
http://localhost:8080/connect/google/callbackandhttp://localhost:8080/connect/microsoft/callbackfor the Docker stack (<API_PUBLIC_URL>/connect/<provider>/callbackelsewhere). -
Scopes: Google
openid email profile https://www.googleapis.com/auth/gmail.modify(with the Gmail API enabled); Microsoft Graph delegatedopenid email profile offline_access User.Read Mail.ReadWrite Mail.Send. -
Put the client ids and secrets in
apps/api/.env.local:GOOGLE_OAUTH_CLIENT_ID=... GOOGLE_OAUTH_CLIENT_SECRET=... MICROSOFT_OAUTH_CLIENT_ID=... MICROSOFT_OAUTH_CLIENT_SECRET=... MICROSOFT_OAUTH_TENANT=common
Without them the web app only offers IMAP accounts.
| Service | Address |
|---|---|
| API | http://localhost:8080 (health check at /health) |
| Web app | http://localhost:5173 |
| Mailpit (outgoing mail) | http://localhost:8025 |
| RustFS (S3) | http://localhost:9000, console on 9001 |
| Meilisearch | http://localhost:7700 |
| Postgres | localhost:54320 (user, password and database: app) |
| Redis | localhost:63790 |
| GreenMail (test mail server) | IMAP localhost:3143, IMAPS 3993, SMTP 3025, SMTPS 3465 |
The sync worker in the dev stack has MAIL_ALLOW_UNSAFE_SERVERS=1 so it can reach GreenMail on the Docker network;
in production that must stay off. The integration tests empty the kim@tresso.test mailbox.
GreenMail has two mailboxes for development, kim@tresso.test (password dev-password-kim) and lisa@tresso.test
(dev-password-lisa). Connect them in the app with host greenmail, IMAP port 3143 and SMTP port 3025, security
none.
ClamAV needs about 1.5 GB of memory, so it only starts when asked. CLAMAV_HOST points the API at it; without it,
attachments are marked as not scanned and open only after a warning. ATTACHMENTS_REQUIRE_SCAN=1 removes that
override (and then needs a scanner: the health check fails without one). Scans run in their own worker, scanner.
CLAMAV_HOST=clamav docker compose --profile scan up -dAfter a signature update, or once clamd is back after scans failed, scan attachments again with
php bin/console app:attachments:rescan --all (or --user=<email>, optionally --status=failed).
Create the test database once:
docker compose exec -e APP_ENV=test api sh -c 'php bin/console doctrine:database:create --if-not-exists && php bin/console doctrine:migrations:migrate -n'Then run the checks for the part you changed:
| Part | Command |
|---|---|
| API tests (per file) | docker compose exec -e APP_ENV=test api php bin/phpunit tests/Functional/SyncLeaseTest.php |
| API static analysis | docker compose exec api sh -c 'bin/console cache:warmup --env=test && vendor/bin/phpstan analyse' |
| API docs (dev only) | http://localhost:8080/docs |
| Sync worker | docker run --rm -v "$PWD/apps/sync":/src -w /src golang:1.27 go test -race ./... |
| Sync worker against GreenMail | docker run --rm --network tresso_default -e GREENMAIL_HOST=greenmail -v "$PWD/apps/sync":/src -w /src golang:1.27 go test -race -tags integration ./... |
| Shared core | npm test --workspace @tresso/core |
| Web | npm run test:unit --workspace @tresso/web and npm run type-check --workspace @tresso/web |
| Desktop | cargo check in apps/desktop/src-tauri (on macOS first node apps/desktop/scripts/fetch-sparkle.mjs, which the Tauri commands also run) |
| Mobile | npx vitest run, npx tsc --noEmit and npx expo lint in apps/mobile |
Every push and pull request runs all of these in CI.
Commits follow Conventional Commits with a required scope from
.agents/commit-scopes.md, for example feat(api): add todo endpoints.
- FrankenPHP keeps the app in memory. After clearing the cache inside the container, restart it with
docker compose restart api. - After
compose.yamlchanges (new environment variables, for example), recreate the services withdocker compose up -d:restartkeeps a container's old environment. - Secrets in
compose.yamland the committed.envfiles are development values only. Real secrets go in.env.localfiles or the deployment environment. In production set at leastAPP_SECRET,OAUTH_PASSPHRASE,OAUTH_ENCRYPTION_KEY,SYNC_WORKER_CLIENT_SECRET,DATA_KEYSandDATA_KEY_CURRENT, the Mercure hub settings (MERCURE_PUBLIC_URL,MERCURE_INTERNAL_URL,MERCURE_CORS_ORIGINSand two different keys of at least 32 bytes inMERCURE_PUBLISHER_JWT_KEYandMERCURE_SUBSCRIBER_JWT_KEY), the object storage settings (S3_ENDPOINT,S3_REGION,S3_BUCKET,S3_ACCESS_KEY,S3_SECRET_KEY), and keepMAIL_ALLOW_UNSAFE_SERVERS=0. - Desktop and mobile sign in through the
tresso://auth/callbackscheme: the system browser signs in and the link brings the result back to the app. Windows and Linux register the scheme whennpm run dev:desktopstarts; macOS only takes it from an app bundle, so to sign in there build one withnpm run tauri --workspace @tresso/desktop -- build --debug --bundles appand open the.appit reports. - The mobile app signs in only from a development build (
npx expo run:iosornpx expo run:androidinapps/mobile): Expo Go would need anexp://redirect, which the authorization server does not accept. The iOS simulator reaches the API onlocalhost; for the Android emulator or a phone setEXPO_PUBLIC_API_URLinapps/mobile/.env.local(http://10.0.2.2:8080, or the computer's address on the network).
Pushing a tag v<major>.<minor>.<patch> runs the release workflow: it builds the
desktop app for macOS (universal), Linux (x86_64 and arm64) and Windows, writes SHA256SUMS, and publishes a GitHub
release. Its notes list the feat, fix and perf commits since the previous release
(scripts/release-notes.mjs). A tag with a suffix (v1.2.0-beta.1) becomes a prerelease, which the install scripts
skip unless you pin it.
git tag -s v0.1.0 -m "Tresso 0.1.0" && git push origin v0.1.0The version comes from the tag, so nothing needs bumping first. The release also publishes the update feeds that installed apps read from the latest release: appcast.xml for Sparkle and latest.json for Tauri's updater (scripts/update-feeds.mjs, which refuses a release with any update unsigned or signed for another version). The release stays a draft until both feeds are uploaded, so no app sees a release without them. Signing the updates needs three repository secrets: SPARKLE_PRIVATE_KEY (EdDSA, macOS), and TAURI_SIGNING_PRIVATE_KEY with TAURI_SIGNING_PRIVATE_KEY_PASSWORD (Windows, Linux). Their public halves are in apps/desktop/src-tauri/Info.plist and tauri.conf.json. The private keys live in 1Password (vault Employee: "Sparkle update signing key (Tresso)" and "Tauri updater signing key (Tresso)"), and the secrets are set from there, for example op read --no-newline "op://Employee/Sparkle update signing key (Tresso)/password" | gh secret set SPARKLE_PRIVATE_KEY. Never rotate them: installed apps trust only these keys, so a new key means they can never update again. A release talks to Tresso Cloud
(https://api.gettresso.com) until the user picks another server. A pull request that changes the release, the desktop
app or the install scripts builds every platform without publishing.
- Architecture: components, self-hosted and Tresso Cloud, auth, sync protocol, processing pipeline, scaling
- Billing: Tresso Cloud's subscriptions, the billing API and Stripe
- Roadmap: phases from foundation to native apps
Tresso is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-only): the apps, the
server and the shared libraries in packages/.