# smtp — Formation Recipe (FvW v8 §38)

**Module ID:** `smtp`
**Module name:** SMTP Client
**Lifecycle phase:** formation
**Status:** INTENT ONLY. No code yet.
**Date:** 2026-07-21

---

## 1. Intent (the "why")
FreshCloud needs a resilient, multi-user SMTP client that enables users to send email through their configured email accounts while maintaining per-user isolation. This module will provide secure email transmission with offline queuing, observability, and audit logging. It serves as the bridge between FreshCloud's user interface and external mail servers, handling authentication, message transmission, and failure recovery with enterprise-grade reliability.

## 2. User-facing behaviour
- **Connect**: Establish authenticated SMTP session per user account
- **Send**: Transmit email messages with headers and body
- **Disconnect**: Gracefully close SMTP connections
- **Queue Offline**: Automatically queue messages when servers are unreachable
- **Retry Failed**: Attempt retransmission from persistent storage
- **Audit Send**: Log transmission attempts with outcomes
- **Status Reports**: Provide send success/failure status to UI

## 3. Public contract (data shapes + signatures)
```typescript
namespace FreshCloud\Mail\SmtpClient;

interface SmtpClientInterface {
  connect(AccountCredentials $credentials): Promise<ConnectionResult>;
  send(
    MessagePayload $message,
    AccountCredentials $credentials
  ): Promise<SendResult>;
  disconnect(): Promise<void>;
  getQueueCount(string $userId, string $accountId): Promise<int>;
  getLastError(): Promise<ErrorResponse>;
}

interface AccountCredentials {
  host: string;
  port: int;
  username: string;
  password: string; // Already decrypted by bridge layer
  encryption: 'tls'|'ssl'|'starttls';
}

interface MessagePayload {
  from: string;
  to: string[];
  cc?: string[];
  bcc?: string[];
  subject: string;
  body: string;
  attachments?: Attachment[];
}

interface SendResult {
  status: 'sent' | 'queued' | 'failed';
  messageId?: string;
  sentAt?: DateTimeImmutable;
  error?: string;
}

interface ConnectionResult {
  success: bool;
  capabilities?: ServerCapability[];
}

interface Attachment {
  filename: string;
  content: string; // Base64 encoded
  mimeType: string;
}
```

## 4. Persistence (what gets stored where)
- **PocketBase Tables**:
  - `email_audit`: Logs all transmission attempts (user_id, account_id, to, subject, status, timestamp)
  - `email_outbox`: Persistent queue for failed/offline sends (user_id, account_id, message_json, retry_count, next_retry_at)
  - `email_accounts`: Stores SMTP configuration (user_id, account_id, smtp_host, smtp_port, username)
- **Local Storage**:
  - Connection pool in-memory (per-process, not persisted)
  - Temporary attachment files (deleted after transmission)

## 5. Dependencies (what this module uses, what uses this)
- **Uses**:
  - PocketBase (authentication + credential storage)
  - Log module (freshcloud.mail.log) for structured audit logging
  - Encryption module (for credential handling at bridge layer)
- **Used By**:
  - User Interface (via React bridge)
  - Background Worker (queue retry mechanism)
  - Admin Panel (SMTP configuration management)

## 6. Behaviour inventory (the N public methods)
| Method | Signature | Purpose | Side Effects | Errors |
|--------|-----------|---------|--------------|--------|
| connect | `(AccountCredentials): Promise<ConnectionResult>` | Establishes authenticated SMTP session | Creates connection in pool | LoginException, ResponseException |
| send | `(MessagePayload, AccountCredentials): Promise<SendResult>` | Transmits email or queues if offline | Writes to audit/outbox tables | SendFailedException, QueueFullException |
| disconnect | `(): Promise<void>` | Closes active SMTP connection | Releases connection from pool | DisconnectionException |
| getQueueCount | `(string userId, string accountId): Promise<int>` | Returns pending send count | Reads from outbox table | |
| getLastError | `(): Promise<ErrorResponse>` | Returns last error details | Reads from session state | |

## 7. State machine
```
[DISCONNECTED] --connect--> [AUTHENTICATED] --send--> [TRANSMITTING] --success--> [DISCONNECTED]
                                                    |
                                                    |--failure--> [QUEUED] --retry--> [AUTHENTICATED]
                                                    |                       |
                                                    |                       |--max retries--> [FAILED]
                                                    |
[DISCONNECTED] --offline--> [QUEUED] --retry--> [AUTHENTICATED]
```
- **States**: DISCONNECTED, AUTHENTICATED, TRANSMITTING, QUEUED, FAILED
- **Triggers**:
  - Connection events: connect/disconnect
  - Transmission events: send success/failure
  - Retry events: background worker attempts
  - Network events: online/offline detection

## 8. Side effects
- **Writes**: 
  - `email_audit` table on every transmission attempt
  - `email_outbox` table on queue failures
  - In-memory connection pool updates
- **Reads**:
  - `email_accounts` for SMTP configuration
  - `email_outbox` for retry attempts
  - PocketBase for credentials (via bridge layer)
- **Emits**:
  - Audit log events (freshcloud.mail.smtp.*)
  - Queue status events (freshcloud.mail.smtp.queue.*)
  - Transmission metrics (freshcloud.mail.smtp.sent|failed|queued)

## 9. Input validation
- **Account Credentials**:
  - Valid domain name for host
  - Port 25, 465, 587, or 2525
  - Non-empty credentials
  - Valid encryption option
- **Message Payload**:
  - RFC5322-compliant email addresses
  - Non-empty subject/body
  - Attachment MIME type validation
- **Queue Operations**:
  - Maximum 1000 items per account (hard stop)

## 10. Failure modes
| Failure Mode | Handling | Caller Experience |
|--------------|----------|-------------------|
| Authentication | Closes connection, logs error | Error: "Authentication failed" |
| Network Timeout | Queues message, increments retry | SendResult: {status: "queued"} |
| Queue Full | Aborts send, marks failed | Error: "Outbox capacity exceeded" |
| TLS Failure | Closes connection, logs error | Error: "Secure connection failed" |
| Attachment Size | Rejects message > 10MB | Error: "Attachment size limit exceeded" |

## 11. User simulation
1. **Online Send**:
   - Given: User has valid SMTP credentials
   - When: Sends email with online connection
   - Then: Message transmitted immediately

2. **Offline Send**:
   - Given: Network connection down
   - When: User attempts email send
   - Then: Message queued, status "queued"

3. **Retry Success**:
   - Given: Previously queued message
   - When: Network restored, worker retries
   - Then: Message transmitted, status "sent"

4. **Retry Failure**:
   - Given: Invalid credentials in queue
   - When: Worker retries after 60s
   - Then: Max retries hit, status "failed"

5. **Multi-Account**:
   - Given: User has 2 SMTP accounts
   - When: Sends from secondary account
   - Then: Correct account credentials used

6. **Attachment Send**:
   - Given: Email with 3 attachments
   - When: Sends via SMTP
   - Then: Message transmitted with MIME encoding

## 12. Reproducibility test
1. Initialize fresh PHP 8.1+ project with Composer
2. Install FreshCloud dependencies (pocketbase, reactphp)
3. Create `FreshCloud\Mail\SmtpClient\` namespace
4. Implement `SmtpClientInterface` with mock socket
5. Configure PocketBase connection locally
6. Populate test SMTP credentials in `email_accounts`
7. Write PHPUnit tests using Greenmail mock server
8. Implement queue retry logic with database polling
9. Add Prometheus metrics endpoint for observability

## 13. Substitutability (FvW v8 §37.4)
A substitute module must provide:
- Identical public interface (SmtpClientInterface)
- Same error contract (LoginException, ResponseException)
- Equivalent audit logging (writes to email_audit)
- Queue persistence (reads/writes email_outbox)
- Per-user isolation (scoped by user_id/account_id)

## 14. Invariants (the 5 things that must always hold)
1. **Isolation**: No state sharing between users or accounts
2. **Encryption**: SMTP credentials never stored plaintext (always encrypted at bridge layer)
3. **Atomicity**: Each send operation is indivisible (all headers transmitted together)
4. **Audit Trail**: Every transmission attempt recorded in email_audit
5. **Queue Persistence**: Failed sends persist to email_outbox until resolved or max retries

## 15. Sweep fields applied (the 14 design intent fields)
### Sweep 1: Multi-user + PocketBase
- **user_scoping** | `SmtpClient` constructor takes `userId` | All operations scoped to user via `user_id` context
- **pb_integration** | Uses PocketBase `email_accounts` table | Credentials fetched from PB, no direct access
- **auth_boundary** | Credentials decrypted in bridge layer | SMTP client never handles raw credentials
- **state_isolation** | Connection pool keyed by `(user_id, account_id)` | One connection per user/account pair
- **logout_behaviour** | Connection pool cleared on user logout | Pending items remain in outbox for retry
- **multi_account** | `SmtpClient` accepts `accountId` parameter | Supports multiple SMTP accounts per user

### Sweep 2: World-class Abilities
- **connection_pool** | `SmtpConnectionPool` class | Small pool with connection reuse per send
- **offline_cache** | `EmailOutbox` persistence layer | Background worker retries queued items
- **sieve_integration** | Not applicable (SMTP is outgoing) | N/A in this module
- **audit_logging** | AuditEvent emitter | Every send logged to `email_audit` table
- **ux_primitives** | `SendResult` enum | UI shows "Sent"/"Queued"/"Failed" status badges

### Sweep 3: Migration + Observability + Tests
- **migration** | SMTP config migration tool | Converts SnappyMail admin panel config to PB fields
- **observability** | Prometheus metrics endpoint | Exposes `smtp_send_duration_seconds` and `smtp_outbox_size`
- **tests** | PHPUnit + Greenmail test suite | 8 test cases covering TLS/auth/queue scenarios

## 16. Cross-references
- §11: Rules for constitutional module structure
- §37: Substitutability requirements
- §38: Formation recipe lifecycle
- §19: Clean-room rebuild from MailSo reference
- `recipes/engine/log/formation.md`: Audit logging integration

## 17. What we are intentionally NOT doing (deferred to future)
- IMAP protocol implementation (separate module)
- Message parsing from MIME (handled by MIME module)
- Sieve server functionality (out of scope)
- Built-in mailbox management (IMAP domain)
- DKIM signing (deferred to future version)
- SMTP relay configuration UI (admin responsibility)
- Rate limiting enforcement (handled upstream)
- Message threading support (UI-level concern)
- Auto-save drafts (handled by drafts module)
- Preview pane rendering (UI concern)
- MailSo had 7 PHP files. We will have 4: `SmtpClient.php`, `Settings.php`, `Exceptions/`, and `ConnectionPool.php`. Reason: Simplified architecture, no legacy drivers.
- Built-in certificate validation (handled by underlying streams)