MailInsight is a full-stack web application that connects to a user's Gmail account(s) via Google OAuth2, fetches unread emails, and uses the Gemini AI to intelligently classify them by category and priority. It surfaces what actually demands attention — interview invites, security alerts, payment notices — while pushing newsletters, promotions, and social noise to the bottom. The result is a searchable, filterable dashboard with charts showing your inbox at a glance.
- Overview
- Tech Stack
- Architecture
- Project Flow
- Core Backend Subsystems
- Database Schema
- API Endpoints
- Security
- Scope and Limitations
- Running Locally
- Environment Variables
A user signs in with Google, links one or more Gmail accounts, and provides their own Gemini API key. When they trigger an analysis, the backend fetches only unprocessed emails (incremental sync by message ID), batches them into groups of 20, and sends each batch to Gemini with a structured prompt. Gemini responds with a category, priority tier, and short summary per email. The results are validated, persisted to PostgreSQL, and rendered on the frontend as interactive charts and a paginated, searchable table.
The project is a modular monolith — a single Spring Boot backend with clearly separated layers (controller → service → repository), rather than microservices. This is a deliberate choice given the project scope and the benefits of simplicity and maintainability.
| Layer | Technology |
|---|---|
| Language / Runtime | Java |
| Framework | Spring Boot |
| Security | Spring Security, Spring Security OAuth2 Client |
| Persistence | Spring Data JPA (Hibernate), PostgreSQL, HikariCP |
| Gmail Integration | Google API Client Library (google-api-client, google-api-services-gmail) |
| AI Integration | Gemini API (gemini-2.0-flash) via RestTemplate, per-user dynamic key injection |
| JSON Handling | Jackson ObjectMapper |
| Encryption | AES-256-GCM (javax.crypto) for OAuth token and API key storage |
| Utilities | Lombok, SLF4J + Logback |
| Build Tool | Maven |
| Layer | Technology |
|---|---|
| Framework | React 18, Vite |
| Routing | React Router v6 |
| Charts | Recharts |
| HTTP | Native fetch with credentialed (cookie-based) requests |
| Styling | CSS Modules |
+----------------------------------------------------------------------+
| CLIENT (Browser) |
| |
| React 18 + Vite |
| +------------+ +-------------+ +--------------+ +----------+ |
| | LoginPage | | OverviewPage| | EmailsPage | | Settings | |
| +-----+------+ +------+------+ +------+-------+ +----+-----+ |
| | | | | |
| +----------------+----------------+---------------+ |
| | |
| fetch() + Session Cookie |
+----------------------------------------------------------------------+
|
HTTP (localhost:8080)
|
+----------------------------------------------------------------------+
| SPRING BOOT BACKEND (Java 17) |
| |
| +------------------------------------------------------------------+|
| | Spring Security Layer ||
| | OAuth2 Client Filter --> CustomOAuth2SuccessHandler ||
| | Session Cookie (JSESSIONID) --> CustomUserDetailsService ||
| +------------------------------------------------------------------+|
| | |
| +----------------------------v-------------------------------------+|
| | Controller Layer ||
| | AuthController | EmailController | AccountController ||
| | UserController ||
| +----------------------------+-+-----------------------------------+|
| | | |
| +----------------------------v-v-----------------------------------+|
| | Service Layer ||
| | ||
| | +--------------+ +-------------+ +------------------+ ||
| | | EmailService | | GmailService| | GeminiService | ||
| | | (orchestrate)|->| (Gmail API) | | (AI Batching) | ||
| | +------+-------+ +------+------+ +--------+---------+ ||
| | | | | ||
| | +------v-------+ +------v------+ +---------v--------+ ||
| | | AiKeyService | | UserService | | EncryptionUtil | ||
| | | (Key Mgmt) | | (User Mgmt) | | (AES-256-GCM) | ||
| | +--------------+ +-------------+ +------------------+ ||
| +----------------------------+-+-----------------------------------+|
| | | |
| +----------------------------v-v-----------------------------------+|
| | Repository Layer ||
| | UserRepo | EmailRepo | ConnectedAccountRepo | AiKeyRepo ||
| +----------------------------+-+-----------------------------------+|
| | | |
+--------------------------------|-|------------------------------------+
| |
+----------------------+ +--------------------+
| |
+------v------+ +----------------+ +-----------v--+
| PostgreSQL | | Gmail API | | Gemini API |
| Database | | (Google) | | (Google AI) |
+-------------+ +----------------+ +--------------+
User visits app
|
v
LoginPage -> "Sign in with Google" button
|
v
Spring Security redirects -> Google OAuth2 Authorization Server
| (scope: openid, email, profile, gmail.readonly)
v
Google returns authorization code
|
v
CustomOAuth2AuthorizedClientService
| +-- Encrypts access_token + refresh_token (AES-256-GCM)
| +-- Stores encrypted tokens in connected_accounts table
|
v
OAuth2SuccessHandler
| +-- Creates or updates User record in users table
| +-- Issues session cookie (JSESSIONID)
|
v
Frontend redirected to /dashboard
User -> Settings -> "Connect another account"
|
v
GET /api/accounts/connect -> returns Google OAuth URL
|
v
User completes OAuth in popup/redirect
|
v
New ConnectedAccount row created for same User
| (each linked account has its own encrypted tokens)
v
Emails from all accounts shown with their source label
User -> Settings -> "Add API Key"
|
v
POST /api/user/api-key { apiKey: "AIza..." }
|
v
AiKeyService
| +-- EncryptionUtil.encrypt(key) -> AES-256-GCM ciphertext
|
v
Stored in user_ai_keys table (never stored in plaintext)
|
v
Frontend shows key status: "Configured"
User -> Dashboard -> "Analyze Emails" button
|
v
POST /api/emails/analyze
|
v
EmailService.analyze()
|
+-- For each linked ConnectedAccount:
| |
| v
| GmailService.fetchUnprocessedEmails()
| | +-- Decrypt access_token from DB
| | +-- Call Gmail API: messages.list (fetch only new IDs)
| | +-- Track processed IDs via UserSyncMetadata
| | +-- Fetch message details for new IDs only
| |
| v
| Threshold check: unprocessed count >= 10?
| | no -> return 422 { current: N, required: 10 }
| | yes -> continue
| |
| v
| Batch emails (20 per batch)
| |
| v
| GeminiService.classifyBatch(emails, userApiKey)
| | +-- EncryptionUtil.decrypt(user stored API key)
| | +-- Build structured JSON prompt with email list
| | +-- POST to Gemini API (gemini-2.0-flash)
| | +-- Parse JSON response
| | +-- Validate: category in allowed_set, priority in [1..5]
| |
| v
| Persist results to emails table
| | (sender, subject, summary, category, priority, accountId)
| |
| v
| Mark batch as processed in UserSyncMetadata
|
v
Return analysis summary { analyzed: N, categories: {...} }
|
v
Frontend refreshes charts + email table
GET /api/emails/stats -> Recharts donut/bar charts (by category & priority)
GET /api/emails -> Paginated email table (sortable, filterable)
GET /api/emails/category/{cat} -> Drill-down view per category
GET /api/emails/new/count -> Progress indicator ("8 of 10 collected")
Incremental sync. UserSyncMetadata tracks the last-seen Gmail history ID and processed message IDs per connected account. On each analysis run, only genuinely new emails are fetched — the entire inbox is never re-scanned.
Batch AI calls. Emails are grouped into batches of 20 and sent to Gemini in a single structured request per batch. This keeps AI call count proportional to batch count, not inbox size, and avoids per-minute rate limit issues.
Minimum threshold guard. Analysis only proceeds when at least 10 unprocessed emails are available. Below that, the endpoint returns HTTP 422 with the current count so the frontend can show "X of 10 collected" — preventing empty or near-useless AI requests.
Response validation. Gemini output is never written to the database blindly. The service validates JSON structure and checks that each email's assigned category and priority are members of the fixed allowed sets. Malformed or partial responses are handled per-email without failing the entire batch.
Bring-your-own-key model. Each user provides their own Gemini API key. It is encrypted with AES-256-GCM using a server-side secret and decrypted in memory only for the duration of an API call. The plaintext key is never stored or logged.
Multi-account support. A single user can link multiple Gmail accounts (personal, work, college). Each account is a separate ConnectedAccount row with its own encrypted OAuth tokens. Emails are tagged with their source account so the dashboard can distinguish them.
Classification taxonomy. Emails are classified into a fixed, curated set of categories grouped by domain (career, interviews, security, banking, learning platforms, newsletters, promotions, campus/community) and one of five priority tiers — from time-sensitive/high-priority down to low-priority/promotional. The taxonomy is fixed server-side to ensure consistent classification quality.
+---------------------+ +---------------------------+
| users | | connected_accounts |
|---------------------| |---------------------------|
| id (PK) |--+ | id (PK) |
| google_id | | | user_id (FK -> users.id) |
| email | +--->| gmail_address |
| name | | access_token (encrypted) |
| picture_url | | refresh_token (encrypted) |
| created_at | | token_expiry |
+---------------------+ +---------------------------+
|
| +-----------------------------+
| | emails |
| |-----------------------------|
| | id (PK) |
| | user_id (FK -> users.id) |
+-->| account_id (FK -> conn_acc) |
| gmail_message_id |
| sender |
| subject |
| ai_summary |
| category |
| priority (1-5) |
| received_at |
| processed_at |
+-----------------------------+
+---------------------+ +-----------------------------+
| user_ai_keys | | user_sync_metadata |
|---------------------| |-----------------------------|
| id (PK) | | id (PK) |
| user_id (FK) | | account_id (FK -> conn_acc) |
| encrypted_api_key | | last_history_id |
| created_at | | processed_message_ids |
| updated_at | | updated_at |
+---------------------+ +-----------------------------+
Note: Only email metadata is persisted — sender, subject, AI-generated summary, category, priority, and timestamps. Raw email bodies and attachments are read from Gmail, used in the Gemini request, and immediately discarded. They are never stored.
| Method | Path | Description |
|---|---|---|
| GET | /api/auth/me |
Returns the currently logged-in user or 401 |
| GET | /api/auth/logout |
Invalidates the session |
| GET | /api/emails |
Paginated, filtered list of analyzed emails |
| GET | /api/emails/new/count |
Count of unprocessed emails (threshold progress) |
| GET | /api/emails/stats |
Aggregate stats by category and priority (for charts) |
| GET | /api/emails/category/{category} |
Paginated emails for a specific category |
| POST | /api/emails/analyze |
Triggers fetch + AI classification pipeline |
| GET | /api/user/api-key/status |
Whether a Gemini API key is configured |
| POST | /api/user/api-key |
Save or update the user's Gemini API key |
| DELETE | /api/user/api-key |
Remove the stored Gemini API key |
| GET | /api/accounts |
List all linked Gmail accounts |
| GET | /api/accounts/connect |
Get the OAuth URL to link an additional Gmail account |
| DELETE | /api/accounts/{id} |
Unlink a Gmail account |
- Authentication: Google OAuth2 only — no passwords are stored or managed by the application.
- Gmail access: Read-only scope (
gmail.readonly). The app cannot send, delete, modify, or label emails. - Token encryption: OAuth access and refresh tokens are encrypted at rest (AES-256-GCM) before being stored in the database.
- API key encryption: Gemini API keys are encrypted at rest and decrypted only in memory, only for the duration of an API call. The plaintext key is never logged or returned to the frontend.
- Sessions: Cookie-based (
JSESSIONID), HTTP-only, invalidated on logout. - CORS: Configurable via
app.cors.allowed-origin; defaults tohttp://localhost:5173in local development. - Error handling: A global
@ControllerAdviceexception handler ensures internal stack traces, tokens, and API keys are never exposed in API responses.
The following are intentionally out of scope to keep the project focused:
- No sending, replying to, deleting, or modifying emails — strictly read-only.
- No attachment parsing — only subject, sender, and body text/snippet are used.
- No real-time push sync — analysis is on-demand, triggered manually by the user.
- No team or multi-tenant workspaces — each Google account maps to one independent user.
- No offline mode or native mobile app.
- No support for non-Gmail providers (Outlook, Yahoo, etc.).
- Java 17+
- Node.js 18+
- PostgreSQL (with a database named
mailanalyzer) - A Google Cloud project with:
- OAuth2 credentials (Web Application type)
- Gmail API enabled
- Authorized redirect URI:
http://localhost:8080/login/oauth2/code/google
- A Gemini API key from Google AI Studio
cd backend
./mvnw spring-boot:runRuns on http://localhost:8080.
cd frontend
npm install
npm run devRuns on http://localhost:5173.
| Property | Description |
|---|---|
spring.datasource.url |
PostgreSQL JDBC URL (e.g. jdbc:postgresql://localhost:5432/mailanalyzer) |
spring.datasource.username |
Database username |
spring.datasource.password |
Database password |
spring.security.oauth2.client.registration.google.client-id |
Google OAuth2 client ID |
spring.security.oauth2.client.registration.google.client-secret |
Google OAuth2 client secret |
app.encryption.secret |
Base64-encoded 32-byte secret for AES-256-GCM encryption |
app.cors.allowed-origin |
Allowed CORS origin (default: http://localhost:5173) |
| Variable | Description |
|---|---|
VITE_API_BASE_URL |
Backend base URL (default: http://localhost:8080) |