NeoMail is a self-hosted AI email client with a CommonJS Node.js backend and a Flutter frontend. It provides multi-user mailbox isolation, IMAP and SMTP connectivity, admin-managed AI providers, a secure external API, and a hosted MCP surface.
server/ Express HTTP server, all backend logic
config/ Static configuration (origins, deployment flags)
db/ SQLite via better-sqlite3
http/ Middleware, routes registration, socket setup, static serving
routes/ One file per feature domain (auth.js, mail.js, admin.js, etc.)
services/ Business logic, grouped by domain
ai/ LLM provider routing and mail assist flows
auth/ API-key, bearer, session, and OAuth auth logic
mail/ Mail accounts, sync, send queue, drafts, search, threading
mcp/ Hosted MCP authorization and tool execution
integrations/ External auth and secret handling
utils/ Shared server-side helpers
runtime/ Startup paths, env resolution, release channel
lib/ Install helpers and migration logic
flutter_app/ Cross-platform Flutter client (web, Android, macOS, Windows, Linux)
lib/src/ Feature-specific Dart source files
scripts/ Build and release utility scripts
- No duplication. Before adding a helper, search
server/utils/,runtime/, and the relevant service directory. Reuse what exists. - No premature abstraction. Three similar lines is fine. Abstract only when a fourth copy would appear.
- No speculative features. Implement exactly what the task requires. No extra fallbacks, feature flags, or backwards-compat shims for callers that don't yet exist.
- No dead code. Remove unused exports, variables, and branches. Do not leave commented-out blocks behind.
- Readable over clever. Prefer direct, boring code. Avoid chained
.reduce, nested ternaries, or dense one-liners that obscure intent.
- CommonJS (
require/module.exports) throughout the server. Do not introduce ESimport/exportin server files. - Every new server file starts with
'use strict';.
- Routes (
server/routes/<domain>.js) handle only HTTP concerns: parse the request, call a service, return a response. No business logic. - Services (
server/services/<domain>/) own all business logic. If a file grows past ~300 lines, split it. - Utils (
server/utils/) are pure helpers with no side effects and no service imports.
- All DB access goes through
server/db/database.js. Never instantiate a secondDatabase. - Use the
better-sqlite3synchronous API. No async wrappers or callbacks. - All queries use parameterized statements. No string interpolation into SQL.
- Schema changes belong in the shared migration path. No
ALTER TABLEorCREATE TABLEinline in service files.
- All LLM calls go through shared AI service entry points. Do not call provider SDKs directly from routes or unrelated services.
- Provider-specific code belongs in
server/services/ai/providers/. - AI behavior must be model-driven over structured message or thread context. Do not add phrase-based filters.
- Validate at system boundaries: HTTP request bodies, external API responses, user-supplied env values. Do not defensively validate arguments passed between internal functions.
- Wrap I/O and external calls in
try/catch. Log errors with a[ServiceName]prefix for searchability. - Every route handler must send a response. Unhandled errors propagate to
server/http/errors.js.
- Never log secrets, tokens, or PII. Log boolean presence (
Boolean(process.env.KEY)) in startup diagnostics. - Parameterized queries everywhere — no SQL string interpolation, ever.
- CORS and origin validation go through
server/config/origins.js. No ad-hoc bypasses. - Server startup must reject (and log clearly) if
SESSION_SECRETis absent. - API keys are stored hashed, scoped, auditable, and revocable. Never persist raw keys after creation.
- Per-user isolation is a hard requirement. No route, service, or MCP tool may read or mutate mailbox data without an owning user context.
- Use the constants exported from
runtime/paths.jsfor all filesystem paths. Never hardcode runtime home directories or absolute system paths. - Use
runtime/env.jshelpers for env access in service code. Rawprocess.envis acceptable only for simple top-level feature flags in entry points.
- Use
server/utils/logger.jsin all service and library code. Plainconsole.logis only acceptable in entry-point startup sequences.
flutter_app/lib/main_controller.dartis the single rootChangeNotifier. Do not introduce competing global state objects.- Feature logic goes in
lib/src/<feature>_bridge.dartorlib/src/<feature>_service.dart. Widgets are thin — no business logic in widget files. - Platform-conditional code uses the established
_io/_web/_stubfile-suffix pattern inlib/src/. Do not usekIsWebinline branches when a conditional export covers the case.
ChangeNotifier+ListenableBuilder/addListeneris the state model. Do not add additional state management packages.- Every
StreamSubscriptionand listener stored as a field must be cancelled/removed indispose().
- Follow the rules in
flutter_app/analysis_options.yaml. - Prefer
final. Uselate finalonly when initialization is genuinely deferred. - Use
constconstructors wherever the analyzer allows. - Avoid
dynamic. When interfacing with untyped data (JSON, JS interop), cast and validate at the boundary.
- Flutter web builds are handled separately as part of the release pipeline. Do not run
flutter build webduring normal development. - For local development use the
flutter:run:webnpm script, which includes the required--dart-defineflags.
| Context | Convention |
|---|---|
| JS filenames | snake_case |
| JS variables / functions | camelCase |
| JS classes | PascalCase |
| Dart filenames | snake_case |
| Dart classes / types | UpperCamelCase |
| Dart variables / functions | lowerCamelCase |
| Environment variables | SCREAMING_SNAKE_CASE |
| HTTP route paths | kebab-case |
| SQLite tables / columns | snake_case |
- Add auth, token, or secret handling in
server/services/integrations/. - Route file at
server/routes/<name>.js— HTTP only, delegate to service. - Register the route in
server/http/routes.js. - Add env keys to
.env.examplewith descriptive comments. - Add presence logging for the new keys in
logStartupConfig()when they are required at startup.
- Define the tool schema and handler in the MCP or AI service layer.
- Extract non-trivial mailbox logic into a mail or AI service module.
- Ensure large tool results are summarized before they are returned to models or clients.
- Do not import Anthropic, OpenAI, or Google AI SDKs outside of
server/services/ai/providers/. - Do not create additional SQLite database files. One DB, one instance.
- Do not commit
.env, runtime data, or anything matched by.gitignore. - Do not bypass git hooks with
--no-verify. - Do not run
flutter build weboutside the release pipeline.