# Technical Design: `mail-smtp-mcp`
## 1) Summary
An MCP server that provides three outcome-oriented tools for sending email via SMTP for pre-configured accounts. Tools use flat, strongly-typed JSON schemas (Zod), return token-efficient summaries, and implement comprehensive security guardrails (no secrets in outputs, least privilege, scrubbed audit logging, and bounded payload sizes).
The server is implemented in TypeScript, uses `nodemailer` for SMTP transport, and communicates via `@modelcontextprotocol/sdk` over stdio. It includes robust validation, policy enforcement, and observability features.
## 2) Goals
- Enable an LLM to send a well-formed email in **one tool call**.
- Provide safe, policy-controlled outbound email sending for configured accounts.
- Keep outputs concise and structured for LLM consumption (no raw MIME by default).
- Enforce strict validation to prevent header injection, path traversal, and accidental exfiltration.
- Support multiple SMTP accounts with clear configuration patterns.
## 3) Non-Goals
- IMAP browsing/searching/reading email (separate bounded context).
- Full MUA features (threading, drafts folders, scheduled send, contact management).
- Provider-specific workflows unless required (e.g., Gmail API specifics).
- Rich templating systems (can be layered outside the server).
- HTTP transport (stdio-only for security).
## 4) Assumptions
- This server runs in a trusted local or controlled environment where credentials can be provisioned securely.
- SMTP accounts are configured via environment variables with the pattern `MAIL_SMTP_<ID>_<SETTING>`.
- Outbound email is inherently irreversible; sending must be guarded by explicit enablement and policy controls.
## 4.1) Implementation Stack
- **MCP:** `@modelcontextprotocol/sdk` version 1.0.0 over **stdio**
- **SMTP:** `nodemailer` version 6.9.16 for SMTP transport
- **Schemas:** `zod` version 3.23.8 with `zod-to-json-schema` for JSON Schema generation
- **Logging:** Custom structured logger with secret redaction
- **Error Handling:** Custom error types with codes (e.g., `CONFIG_MISSING`, `VALIDATION_ERROR`, `POLICY_VIOLATION`)
- **Package Manager:** `pnpm`
- **Runtime:** Node.js >=20.0.0
- **Build:** TypeScript compiler (tsc)
## 5) Constraints & Design Rules
- **Tool count:** 3 tools (keeps surface small and safe).
- **Outcome-oriented tools:** each tool maps to a complete user capability.
- **Input schemas:** flat inputs; Zod validation with enforced bounds.
- **Outputs:** concise JSON with `summary`, `data`, and optional `_meta`.
- **Security:** never return secrets; scrub secrets from logs; least privilege; strict size limits.
- **Sending gate:** sending must be disabled by default and explicitly enabled via `MAIL_SMTP_SEND_ENABLED=true`.
- **Hard maximums:** absolute upper bounds that cannot be exceeded regardless of policy configuration.
## 6) Bounded Context
**"Send outbound email messages over SMTP for configured accounts."**
Primary user outcomes:
- "Email Alice a short status update from my work account."
- "Send an email with a small attachment (size-limited) to a known recipient."
- "Verify my SMTP account is configured and can connect/authenticate."
## 7) Tool Surface
### 7.1 Tool list
1. `smtp_list_accounts`
2. `smtp_verify_account`
3. `smtp_send_message`
### 7.2 Common conventions
- All tools accept `account_id: string` (defaults to `"default"` if omitted).
- All tools return a `ToolResponse<T>` structure:
```typescript
{
summary: string; // Human-readable status
data: T; // Tool-specific data
_meta?: Record<string, unknown>; // Optional metadata
}
```
- Never echo credentials; never include SMTP AUTH details in outputs.
- Tools return actionable, non-sensitive errors with error codes.
### 7.3 Tool contracts
#### `smtp_list_accounts`
Purpose: enumerate configured SMTP accounts (non-secret metadata only).
Input:
```typescript
{
account_id?: string; // Optional filter
}
```
Output `data`:
```typescript
{
accounts: Array<{
account_id: string;
host: string;
port: number;
secure: boolean;
default_from?: string;
}>;
}
```
Response includes `_meta` with:
- `send_enabled`: boolean
- `limits`: current policy limits
#### `smtp_verify_account`
Purpose: check config + connectivity/auth (no email sent).
Input:
```typescript
{
account_id: string; // defaults to "default"
}
```
Output `data`:
```typescript
{
account_id: string;
status: "ok";
}
```
On failure, returns error with actionable message (no secrets).
#### `smtp_send_message`
Purpose: send an outbound email (text and/or HTML), optionally with attachments.
Input:
```typescript
{
account_id: string; // defaults to "default"
from?: string; // override defaultFrom
to: string | string[]; // required
cc?: string | string[];
bcc?: string | string[];
reply_to?: string;
subject: string; // required, max 256 chars
text_body?: string; // optional, max chars per policy
html_body?: string; // optional, max chars per policy
attachments?: Array<{
filename: string; // required, max 256, no path traversal
content_base64: string; // required
content_type?: string; // optional, max 128
}>;
dry_run?: boolean; // default false
}
```
Output `data`:
```typescript
{
account_id: string;
dry_run: boolean;
envelope: {
from: string;
to: string[];
cc?: string[];
bcc?: string[];
};
message_id?: string; // from transport if available
accepted?: string[]; // from transport
rejected?: string[]; // from transport
size_bytes_estimate?: number;
}
```
## 8) Data Model
### 8.1 Internal Types
**AccountConfig** (from environment, never emitted):
```typescript
{
accountId: string;
host: string;
port: number;
secure: boolean;
user: string;
pass: string;
defaultFrom?: string;
}
```
**PolicyConfig** (controls and limits):
```typescript
{
sendEnabled: boolean;
allowlistDomains: readonly string[];
allowlistAddresses: readonly string[];
maxRecipients: number;
maxMessageBytes: number;
maxAttachmentBytes: number;
maxAttachments: number;
maxTextChars: number;
maxHtmlChars: number;
connectTimeoutMs: number;
socketTimeoutMs: number;
}
```
**NormalizedRecipients** (validated addresses):
```typescript
{
readonly to: readonly string[];
readonly cc: readonly string[];
readonly bcc: readonly string[];
}
```
### 8.2 Validation Layers
1. **Hard Maximums** (enforced by Zod schemas):
- `HARD_MAX_RECIPIENTS = 50`
- `HARD_MAX_ATTACHMENTS = 10`
- `HARD_MAX_ATTACHMENT_BYTES = 5_000_000`
- `HARD_MAX_TEXT_CHARS = 100_000`
- `HARD_MAX_HTML_CHARS = 200_000`
- `HARD_MAX_SUBJECT_CHARS = 256`
2. **Policy Limits** (configurable via environment, defaults shown):
- `max_recipients = 10`
- `max_message_bytes = 2_500_000`
- `max_attachments = 5`
- `max_attachment_bytes = 2_000_000`
- `max_text_chars = 20_000`
- `max_html_chars = 50_000`
- `connect_timeout_ms = 10_000`
- `socket_timeout_ms = 20_000`
## 9) Security & Privacy
### 9.1 Credential Handling
- Never include passwords, tokens, or credentials in tool outputs.
- `redactSecrets()` utility automatically redacts keys containing: `password`, `pass`, `token`, `secret`, `authorization`, `cookie`, `key`.
- Credentials only exist in memory as `AccountConfig` and are passed to `nodemailer`.
### 9.2 Send Gate Enforcement
- Default `MAIL_SMTP_SEND_ENABLED=false`.
- `smtp_send_message` rejects when disabled (even for valid payloads).
- `dry_run=true` is permitted when send is disabled.
- Checked before SMTP transport creation.
### 9.3 Header Injection Prevention
- `containsCarriageReturnOrLineFeed()` detects CR/LF in email addresses and subject.
- Email addresses validated with regex pattern (rejects whitespace, requires `@` and domain with dot).
- Rejects angle brackets (`<`, `>`) to prevent RFC 5322 addr-spec confusion.
### 9.4 Path Traversal Prevention
- `isSafeFilename()` validates attachment filenames:
- Length: 1-256 chars
- No `/`, `\`, or `..` sequences
### 9.5 HTML Handling
- HTML passed through as content (no sanitization for sending).
- Logs and outputs use character counts, not full content.
### 9.6 Recipient Policy
- If no allowlist configured, allow all outbound (subject to limits).
- If configured, enforce:
- `MAIL_SMTP_ALLOWLIST_DOMAINS` (comma-separated)
- `MAIL_SMTP_ALLOWLIST_ADDRESSES` (comma-separated)
- Recipient matches if either full address or domain is in allowlist.
## 10) Observability & Operations
### 10.1 Structured Logging
Custom logger with:
- `logger.audit()` for tool invocations with:
- Tool name
- Duration in milliseconds
- Scrubbed arguments (for `smtp_send_message`: counts only)
- Error status if applicable
- `logger.info()`, `logger.error()` for other events
### 10.2 Argument Summarization
For `smtp_send_message`, audit logs include metrics instead of raw data:
```typescript
{
account_id: string;
dry_run: boolean;
to_count: number;
cc_count: number;
bcc_count: number;
subject_chars: number;
text_body_chars: number;
html_body_chars: number;
attachments_count: number;
attachments_base64_chars: number;
}
```
### 10.3 Help System
Server supports `--help` flag to display:
- Configured accounts (with secrets redacted)
- Environment variable documentation
- Current policy defaults
## 11) Error Handling
### 11.1 Error Types
Custom `ToolError` with error codes:
- `CONFIG_MISSING`: Account not configured or missing required env vars
- `VALIDATION_ERROR`: Invalid email address, missing required field, etc.
- `POLICY_VIOLATION`: Recipient blocked, size limit exceeded, etc.
- `ATTACHMENT_ERROR`: Invalid filename, invalid base64, too large, etc.
- `SEND_DISABLED`: Sending disabled but `dry_run=false`
### 11.2 Error Response Format
All errors returned as:
```typescript
{
summary: string;
error: {
message: string;
code?: string;
};
}
```
With `isError: true` in MCP response.
## 12) Project Structure
```
mail-smtp-mcp/
├── src/
│ ├── index.ts # Entry point, help system, startup
│ ├── server.ts # MCP server creation, tool registration, audit wrappers
│ ├── config.ts # Environment loading, AccountConfig, PolicyConfig
│ ├── schemas.ts # Zod schemas for inputs/outputs, hard maximums
│ ├── startup.ts # Startup validation, env checking
│ ├── logger.ts # Structured logging with secret redaction
│ ├── errors.ts # ToolError, error formatting
│ ├── policy.ts # Recipient normalization, allowlist enforcement
│ ├── tools/
│ │ ├── list_accounts.ts # smtp_list_accounts handler
│ │ ├── verify_account.ts # smtp_verify_account handler
│ │ ├── send_message.ts # smtp_send_message handler
│ │ └── response.ts # Response formatting helpers
│ └── utils/
│ ├── base64.ts # Strict base64 decoding
│ ├── email.ts # Email validation, normalization
│ ├── sizes.ts # Byte length, message size estimation
│ └── strings.ts # String utilities, CSV parsing, secret redaction
├── tests/ # Vitest test suites
├── dist/ # Build output (generated)
└── docs/ # Documentation
```
## 13) Testing Strategy
### 13.1 Test Coverage
Test files follow naming convention `<tool-name>.test.ts`:
- `tests/list-accounts.test.ts`
- `tests/verify-account.test.ts`
- `tests/send-message.test.ts`
- `tests/startup.test.ts`
- `tests/tools-list.test.ts`
Minimum coverage per tool:
- Valid input → expected result shape and summary
- Invalid input (schema violations) → `isError: true` with actionable message
- Send-gate enforcement (`MAIL_SMTP_SEND_ENABLED`)
- `dry_run` behavior (validates without sending)
- Recipient allowlist policy (allowed vs blocked)
- Attachment limits (count, total bytes, per-attachment bytes)
- Character limits for subject and bodies
### 13.2 Test Utilities
- Arrange-Act-Assert pattern
- Data-driven test cases for edge cases
- Deterministic and fast execution
## 14) Versioning & Compatibility
- Package name: `@bradsjm/mail-smtp-mcp`
- Current version: `0.1.0`
- Semantic versioning `MAJOR.MINOR.PATCH`
- Tool names and required fields are the stable API contract
- Deprecate before removal; document migrations for schema changes
## 15) Environment Variables
### 15.1 Account Configuration
Pattern: `MAIL_SMTP_<ACCOUNT_ID>_<SETTING>`
Required per account:
- `MAIL_SMTP_<ID>_HOST` - SMTP server hostname
- `MAIL_SMTP_<ID>_USER` - SMTP authentication username
- `MAIL_SMTP_<ID>_PASS` - SMTP authentication password
Optional per account:
- `MAIL_SMTP_<ID>_PORT` - SMTP port (defaults: 465 if SECURE=true, 587 if SECURE=false)
- `MAIL_SMTP_<ID>_SECURE` - Use SSL/TLS (true) or STARTTLS (false); default false
- `MAIL_SMTP_<ID>_FROM` - Default sender address
Example accounts:
```bash
# Default account
MAIL_SMTP_DEFAULT_HOST=smtp.gmail.com
MAIL_SMTP_DEFAULT_PORT=587
MAIL_SMTP_DEFAULT_SECURE=false
MAIL_SMTP_DEFAULT_USER=you@gmail.com
MAIL_SMTP_DEFAULT_PASS=your-app-password
MAIL_SMTP_DEFAULT_FROM=you@gmail.com
# Work account
MAIL_SMTP_WORK_HOST=smtp.company.com
MAIL_SMTP_WORK_PORT=465
MAIL_SMTP_WORK_SECURE=true
MAIL_SMTP_WORK_USER=you@company.com
MAIL_SMTP_WORK_PASS=company-password
MAIL_SMTP_WORK_FROM=you@company.com
```
### 15.2 Global Controls
```bash
MAIL_SMTP_SEND_ENABLED=false # Enable sending (default false)
MAIL_SMTP_ALLOWLIST_DOMAINS= # Comma-separated allowed domains (optional)
MAIL_SMTP_ALLOWLIST_ADDRESSES= # Comma-separated allowed addresses (optional)
```
### 15.3 Policy Limits
```bash
MAIL_SMTP_MAX_RECIPIENTS=10
MAIL_SMTP_MAX_MESSAGE_BYTES=2500000 # ~2.4 MB
MAIL_SMTP_MAX_ATTACHMENTS=5
MAIL_SMTP_MAX_ATTACHMENT_BYTES=2000000 # ~1.9 MB per file
MAIL_SMTP_MAX_TEXT_CHARS=20000
MAIL_SMTP_MAX_HTML_CHARS=50000
```
### 15.4 Timeouts
```bash
MAIL_SMTP_CONNECT_TIMEOUT_MS=10000 # 10 seconds
MAIL_SMTP_SOCKET_TIMEOUT_MS=20000 # 20 seconds
```
## 16) Development & Build
### 16.1 Commands
```bash
pnpm install # Install dependencies
pnpm dev # Run from TypeScript using tsx
pnpm build # Compile TypeScript to dist/
pnpm start # Run compiled server
pnpm lint # ESLint with zero warnings
pnpm format # Prettier check (use pnpm format:write to fix)
pnpm typecheck # TypeScript checks for app + tests
pnpm test # Run Vitest in CI mode
pnpm check # Run format:check + lint + typecheck + test
```
### 16.2 Code Style
- TypeScript with strict typing; avoid `any`.
- Formatting enforced by Prettier.
- ESLint must pass with `--max-warnings 0`.
- Naming: `camelCase` for variables/functions, `PascalCase` for types/classes, `kebab-case` for test filenames.
## 17) Integration Notes
### 17.1 MCP Client Configuration
Most MCP clients can spawn via npx:
```json
{
"command": "npx",
"args": ["-y", "@bradsjm/mail-smtp-mcp"],
"env": {
"MAIL_SMTP_DEFAULT_HOST": "smtp.gmail.com",
"MAIL_SMTP_DEFAULT_USER": "you@gmail.com",
"MAIL_SMTP_DEFAULT_PASS": "your-app-password",
"MAIL_SMTP_SEND_ENABLED": "true"
}
}
```
For local development:
```json
{
"command": "node",
"args": ["/path/to/mail-smtp-mcp/dist/index.js"],
"env": { /* ... */ }
}
```
### 17.2 Startup Validation
Server checks environment before starting:
- At least one account must be configured (`MAIL_SMTP_<ID>_HOST` present)
- All required fields (HOST, USER, PASS) must be present for each account
- Exits with code 1 and helpful error message if validation fails
## 18) License
MIT License