A full-featured webmail client built with the Vio framework. Connect multiple Gmail and Outlook accounts, manage folders, search messages, view conversation threads, and integrate with AI agents through MCP devtools.
- Multi-account support — Connect Gmail and Outlook accounts via OAuth 2.0
- Three-panel layout — Sidebar, message list, and message detail view
- Local SQLite cache — Messages are cached locally for fast offline-capable browsing
- Incremental sync — Background sync every 60 seconds using Gmail History API and Microsoft Delta API
- Conversation threads — Group messages by thread with a stacked conversation view (toggle in settings)
- Folder management — Create and delete custom folders/labels synced to your email provider
- Move messages — Move messages between folders via a dropdown in the message view
- Search — Search across subject, sender, and body text with a clear button to reset
- Compose, reply, and forward — Full compose form with reply/forward prefilling
- Star and read status — Toggle starred/read state with best-effort provider sync
- MCP integration — AI agents can manage folders, move messages, and apply labels via vio-devtools
- Permission model — Per-account MCP access levels:
read_organizeorfull_control
| Layer | Technology |
|---|---|
| Frontend | Vio (reactive component framework) |
| Backend | Fastify 5.x |
| Database | SQLite via better-sqlite3 |
| Gmail API | googleapis |
| Outlook API | @azure/msal-node + Microsoft Graph |
| Build | Vite 7.x |
| Language | TypeScript 5.x |
| Tests | Vitest |
| DevTools | vio-devtools (MCP integration) |
- Node.js 20+
- A Google Cloud project with the Gmail API enabled (for Gmail accounts)
- A Microsoft Azure app registration with Mail.ReadWrite + Mail.Send permissions (for Outlook accounts)
- The Vio framework built locally at
../vio-framework/packages/vio
npm installCreate a .env file in the project root:
# Server
PORT=3001
DB_PATH=vmail.db
# Google OAuth (Gmail)
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GOOGLE_REDIRECT_URI=http://localhost:3001/auth/google/callback
# Microsoft OAuth (Outlook)
MICROSOFT_CLIENT_ID=your-microsoft-client-id
MICROSOFT_CLIENT_SECRET=your-microsoft-client-secret
MICROSOFT_REDIRECT_URI=http://localhost:3001/auth/microsoft/callback- Go to Google Cloud Console
- Create a project and enable the Gmail API
- Create OAuth 2.0 credentials (Web application)
- Add
http://localhost:3001/auth/google/callbackas an authorized redirect URI - Copy the client ID and secret to your
.env
Required scopes (requested automatically):
gmail.modify— Read and modify messagesgmail.compose— Create draftsgmail.send— Send messagesuserinfo.email/userinfo.profile— User info
- Go to Azure Portal > App registrations
- Register a new application
- Add a Web redirect URI:
http://localhost:3001/auth/microsoft/callback - Under API permissions, add:
Mail.ReadWrite,Mail.Send,openid,profile,email,offline_access - Create a client secret and copy it along with the client ID to your
.env
npm run devThis starts both:
- Backend on
http://localhost:3001(Fastify + SQLite) - Frontend on
http://localhost:4005(Vite dev server, proxies/apiand/authto backend)
Open http://localhost:4005 in your browser.
Click Add Gmail or Add Outlook in the sidebar. You'll be redirected to the provider's OAuth consent screen. After authorizing, vmail syncs your folders and fetches the 30 most recent inbox messages.
- Sidebar — Switch between accounts and folders
- Message list — Click a message to view it; unread messages appear bold
- Search — Type in the search bar and press Enter; click the x button to clear
- Compose — Click the Compose button to open the compose form
- Click the + button next to "Folders" in the sidebar to create a new folder
- Type a name and press Enter (or Escape to cancel)
- Hover over a custom folder to reveal the x delete button
- Use the Move to... dropdown in the message view to move messages between folders
- Go to Settings and enable conversation view
- Messages in the list are grouped by thread with a count badge
- Click a thread to see all messages stacked chronologically in the detail view
- Conversation view — Toggle thread grouping on/off
- MCP Access — Set per-account permission level for AI agent access
- Remove Account — Delete an account and all its cached data
src/
├── shared/
│ └── types.ts # Shared TypeScript interfaces (Message, Folder, Account, etc.)
├── server/
│ ├── index.ts # Fastify server entry point
│ ├── config.ts # Environment configuration
│ ├── db/
│ │ ├── schema.ts # SQLite schema (accounts, folders, messages, settings)
│ │ └── index.ts # Database initialization + migrations
│ ├── providers/
│ │ ├── types.ts # EmailProvider interface
│ │ ├── registry.ts # Provider registry (lookup by name)
│ │ ├── google.ts # Gmail provider (OAuth, labels, messages, sync)
│ │ └── microsoft.ts # Outlook provider (MSAL, Graph API, delta sync)
│ └── routes/
│ ├── auth.ts # OAuth flow (/auth/:provider, /auth/:provider/callback)
│ ├── accounts.ts # Account CRUD (/api/accounts)
│ ├── emails.ts # Messages, folders, search, sync, settings, MCP
│ └── static.ts # Production static file serving
└── client/
├── main.ts # App root, routing, mount, MCP dispatch bridge
├── api.ts # Fetch wrapper for all API endpoints
├── styles.css # Full application styles
├── store/
│ └── actions.ts # Vio store state + actions (reducers)
└── components/
├── Sidebar.ts # Account switcher, folder list, folder CRUD
├── MessageList.ts # Message/thread list with search
├── MessageView.ts # Message detail, thread view, action bar
├── ComposeView.ts # Compose/reply/forward form
└── SettingsView.ts # Account settings, MCP permissions, display options
| Table | Purpose |
|---|---|
accounts |
OAuth credentials, sync tokens, user info |
folders |
Email folders/labels per account (unique: account_id + provider_folder_id) |
messages |
Full message cache with thread_id, indexed by account, folder, date, thread |
attachments |
Message attachments (foreign key to messages) |
drafts |
Unsent draft messages |
mcp_settings |
MCP permission level per account |
user_settings |
Key-value user preferences (e.g., conversation view toggle) |
- On auth — Fetch all folders + 30 recent inbox messages + initial sync token
- On folder open — If no cached messages, fetch from provider on demand
- Periodic sync — Every 60 seconds, incremental sync via Gmail History API or Microsoft Delta API
- Graceful degradation — If sync token expires, resets and re-syncs
| Method | Path | Description |
|---|---|---|
GET |
/auth/providers |
List available OAuth providers |
GET |
/auth/:provider |
Redirect to provider OAuth consent screen |
GET |
/auth/:provider/callback |
OAuth callback — exchanges code, creates account, initial sync |
| Method | Path | Description |
|---|---|---|
GET |
/api/accounts |
List all accounts |
GET |
/api/accounts/:id |
Get single account |
DELETE |
/api/accounts/:id |
Delete account (cascades) |
| Method | Path | Description |
|---|---|---|
GET |
/api/accounts/:accountId/folders |
List folders for account |
POST |
/api/accounts/:accountId/folders |
Create folder { name } |
DELETE |
/api/accounts/:accountId/folders/:folderId |
Delete folder |
| Method | Path | Description |
|---|---|---|
GET |
/api/accounts/:accountId/folders/:folderId/messages |
List messages (paginated: ?page=&limit=) |
GET |
/api/accounts/:accountId/messages/:messageId |
Get full message |
PATCH |
/api/accounts/:accountId/messages/:messageId |
Update flags { isRead, isStarred } |
POST |
/api/accounts/:accountId/messages/:messageId/move |
Move message { folderId } |
DELETE |
/api/accounts/:accountId/messages/:messageId |
Delete message |
GET |
/api/accounts/:accountId/search?q= |
Search messages |
GET |
/api/accounts/:accountId/threads/:threadId/messages |
Get all messages in a thread |
| Method | Path | Description |
|---|---|---|
POST |
/api/accounts/:accountId/send |
Send message { to, cc, bcc, subject, body, replyToId? } |
| Method | Path | Description |
|---|---|---|
POST |
/api/accounts/:accountId/sync |
Trigger incremental sync |
| Method | Path | Description |
|---|---|---|
GET |
/api/settings |
Get all user settings |
PUT |
/api/settings |
Update user settings { key: value } |
GET |
/api/accounts/:accountId/mcp-settings |
Get MCP permission level |
PUT |
/api/accounts/:accountId/mcp-settings |
Set MCP permission { permissionLevel } |
| Method | Path | Description |
|---|---|---|
GET |
/api/health |
Health check { status: "ok" } |
vmail integrates with AI agents through vio-devtools. Agents can dispatch actions to the Vio store, which are intercepted by a bridge that triggers the corresponding API calls.
| Action | Payload | Permission |
|---|---|---|
mcpCreateFolder |
{ name: string } |
full_control |
mcpDeleteFolder |
{ folderId: string } |
full_control |
mcpMoveMessage |
{ messageId: string, folderId: string } |
read_organize |
mcpAddLabel |
{ messageId: string, labelId: string } |
read_organize |
mcpRemoveLabel |
{ messageId: string, labelId: string } |
read_organize |
mcpSendMessage |
{ to, subject, body, ... } |
full_control |
mcpDeleteMessage |
messageId |
full_control |
mcpSaveDraft |
{ to, subject, body, ... } |
full_control |
vio_dispatch('mcpCreateFolder', { name: 'Receipts' })
vio_dispatch('mcpMoveMessage', { messageId: '...', folderId: '...' })
- read_organize — Read, search, star, move, delete messages; manage labels
- full_control — All of the above plus send, reply, forward, create/delete folders, save drafts
Set per-account in Settings > MCP Access.
# Run all tests
npm test
# Watch mode
npm run test:watchTests cover:
- Database creation and migrations
- Provider registry
- Google provider (mocked Gmail API)
- Microsoft provider (mocked Graph API)
- Server health endpoint
- Account CRUD routes
- Email routes (list, search, update, move, delete, send, sync)
- Integration tests (full request lifecycle)
npm run buildThis compiles:
- Client →
dist/client/(Vite build) - Server →
dist/server/(TypeScript compilation)
Run in production:
NODE_ENV=production node dist/server/index.jsThe server serves the built client files and handles SPA fallback routing.
MIT