# mail-client — Formation Recipe (FvW v8 §38)

**Module ID:** `mail-client`
**Module name:** FreshCloud Mail Client Facade
**Lifecycle phase:** formation
**Status:** INTENT ONLY. No code yet.
**Date:** 2026-07-21

---

## 1. Intent (the "why")
FreshCloud Mail needs a clean-room PHP email abstraction layer that:  
- Replaces MailSo's AGPLv3 code with MIT-licensed alternatives  
- Scales to multi-user PocketBase architectures  
- Enables offline-first UX with encrypted caches  
- Provides auditability for billing/compliance  
- Simplifies maintenance by removing non-core features (complex HTML rendering, deprecated protocols)  
- Serves as the central facade for React UI inbox operations  

## 2. User-facing behaviour
- **Inbox Management**: Fetch paginated message lists with unread counts and cursors  
- **Message Operations**: View messages, mark as read, delete, move, star  
- **Sending**: Compose/send emails with attachments via SMTP  
- **Folder Sync**: List IMAP folders with sync status  
- **Rules Engine**: Fetch/set server-side filtering rules via Sieve  
- **Account Management**: Switch between multiple email accounts  
- **Offline Access**: Browse cached messages during network outage  

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

interface MailClient {
  // User Account Scoping
  getInbox(userId: string, accountId: string, pageToken?: string): Promise<InboxResult>;
  getFolders(userId: string, accountId: string): Promise<FolderCollection>;
  
  // Message Operations
  getMessage(userId: string, accountId: string, folder: string, uid: number): Promise<Message>;
  getMessageList(userId: string, accountId: string, folder: string, pageToken?: string): Promise<MessageListResult>;
  markMessage(userId: string, accountId: string, folder: string, uid: number, flag: string): Promise<boolean>;
  moveMessage(userId: string, accountId: string, srcFolder: string, uid: number, destFolder: string): Promise<boolean>;
  
  // Sending
  sendMessage(userId: string, accountId: string, message: DraftMessage): Promise<SendResult>;
  saveDraft(userId: string, accountId: string, message: DraftMessage): Promise<string>;
  
  // Rules & Sync
  getSieveRules(userId: string, accountId: string): Promise<SieveRuleCollection>;
  setSieveRule(userId: string, accountId: string, rule: SieveRule): Promise<boolean>;
  syncAccount(userId: string, accountId: string): Promise<SyncResult>;
  
  // Cache Control
  clearCache(userId: string, accountId: string): Promise<boolean>;
}

interface InboxResult {
  messages: Message[];
  total_unread: number;
  total_count: number;
  has_more: boolean;
  next_page_token: string;
}

interface Message {
  uid: number;
  subject: string;
  from: Contact;
  date: Date;
  is_read: boolean;
  is_starred: boolean;
  body: BodyParts;
  attachments: Attachment[];
  folder: string;
}

interface BodyParts {
  text: string;
  html: string;
}

interface Attachment {
  name: string;
  mime_type: string;
  size: number;
  cid?: string;
}

interface SieveRule {
  name: string;
  conditions: Condition[];
  actions: Action[];
}
```

## 4. Persistence (what gets stored where)
- **PocketBase Tables**:  
  - `email_audit`: Every user action (read, send, delete)  
  - `email_drafts`: Draft messages with user_id/account_id keys  
  - `email_accounts`: Account credentials (encrypted) + `is_default` flag  
- **Local Storage**:  
  - Encrypted message cache per (user_id, account_id, folder)  
  - Session state for connection pool (recreated after restart)  
- **Temporary Files**:  
  - Attachment previews during upload  
  - Message body caches during streaming  

## 5. Dependencies (what this module uses, what uses this)
**Uses**:  
- Engine: `mime-content` (MIME parsing), `log` (structured logging)  
- External: `pocketbase` (auth/storage), `reactphp/socket` (async connections)  
- Test: `greenmail` (mock IMAP/SMTP), `phpunit`  

**Used by**:  
- App: `ui-bridge` (React mailbox interface)  
- Engine: `notification-service` (email-based alerts)  

## 6. Behaviour inventory (the N public methods)
| Method | Signature | Purpose | Side Effects | Errors |
|--------|-----------|---------|--------------|--------|
| `getInbox` | `Promise<InboxResult>` | Fetch paginated inbox | Updates `last_sync` in PB | AccountNotFound |
| `getMessage` | `Promise<Message>` | Fetch single message | Caches decrypted body | MessageNotFound |
| `sendMessage` | `Promise<SendResult>` | Send email via SMTP | Writes to `email_audit`, increments `sent_count` | AuthenticationFailed |
| `getSieveRules` | `Promise<SieveRule[]>` | Fetch server rules | Writes audit event | ServerNotSupportSieve |
| `moveMessage` | `Promise<boolean>` | Move between folders | Updates PB `email_messages` | PermissionDenied |

## 7. State machine
```mermaid
stateDiagram-v2
    [*] --> Disconnected
    Disconnected --> Connecting : connect(userId, accountId)
    Connecting --> Connected : auth_success
    Connecting --> Disconnected : auth_fail
    Connected --> Active : first_operation
    Active --> Syncing : syncRequest
    Syncing --> Active : sync_complete
    Active --> Offline : network_loss
    Offline --> Active : network_restored
    Active --> Disconnected : disconnectUser(userId)
    Disconnected --> [*]
```

## 8. Side effects
- **Writes to**:  
  - `email_audit` (audit_log_entry: action=result)  
  - `email_accounts.last_sync` (timestamp on sync)  
  - `email_drafts` (new draft)  
- **Creates**:  
  - Encrypted cache files in `/cache/messages/{user_id}/{account_id}/{folder}`  
  - Temporary upload files in `/tmp/attachments`  
- **Emits**:  
  - `activity_events` with `user_id` and `operation_type`  
  - `connection_status` events to UI bridge  

## 9. Input validation
- `user_id`: Must be valid UUID from PocketBase session  
- `account_id`: Must exist in `email_accounts` for user  
- `folder`: Must be valid IMAP folder name (RFC 3501 compliant)  
- `message.uid`: Must be > 0 and exist in folder  
- `DraftMessage.subject`: Max 255 characters  
- `SieveRule`: Max 10 conditions per rule  

## 10. Failure modes
| Failure Type | Handling | Caller Response |
|--------------|----------|-----------------|
| Connection Timeout | Retry 3x → error | Fallback to cache or show error UI |
| Authentication Failed | Log audit event | Prompt for credentials |
| Quota Exceeded | Log audit event | Show "Storage Full" UI |
| Malformed Email | Reject with RFC errors | Highlight validation errors |
| Cache Corrupted | Purge cache + resync | Refresh UI data |

## 11. User simulation
**Scenario 1: Inbox Load**  
*Given* User 123 connects with Gmail account  
*When* They open inbox page 1  
*Then* System shows 20 messages with 5 unread  

**Scenario 2: Offline Mode**  
*Given* Network disconnect while viewing inbox  
*When* User scrolls to next page  
*Then* Shows cached messages with "Offline" banner  

**Scenario 3: Send Email**  
*Given* User composes email with attachment  
*When* They click send  
*Then* Email queued, audit logged, attachment uploaded  

**Scenario 4: Account Migration**  
*Given* User migrates from SnappyMail  
*When* Trigger migration process  
*Then* All accounts/preferences transferred to PB  

## 12. Reproducibility test
1. Create PocketBase instance with freshcloud schema  
2. Install dependencies via composer  
3. Run `php bin/console mail-client:create-user test@example.com`  
4. Configure mock Gmail account in PB `email_accounts`  
5. Execute `php test/sync-account.php test@example.com`  
6. Verify `/cache/messages/test@example.com/INBOX` contains 10 emails  
7. Send test email via `php test/send-email.php`  
8. Check `email_audit` for `action=send` entry  
9. Run PHPUnit tests `./vendor/bin/phpunit tests/mail-client`  

## 13. Substitutability (FvW v8 §37.4)
Must provide:  
- Same public method signatures  
- Equivalent `InboxResult` data shape  
- Identical audit event structure  
- Local cache decryption using PB-derived keys  
- Multi-account scoping via (user_id, account_id)  

## 14. Invariants (the 5 things that must always hold)
1. All entry points require (user_id, account_id) for scoping  
2. Passwords never stored in plaintext (PB handles encryption)  
3. Network operations timeout after 30 seconds  
4. Cache files invalidated on account credentials change  
5. Every email operation generates audit entry  

## 15. Sweep fields applied (the 14 design intent fields)
### Sweep 1: Multi-user + PocketBase (6 fields)
- **user_scoping**: Every method takes (user_id, account_id). Implements `getUserSession()` PB lookup.  
- **pb_integration**: Direct CRUD to `email_accounts`, `email_audit`, `email_drafts`.  
- **auth_boundary**: Trusts PB session; no auth logic here.  
- **state_isolation**: Cache keys = hash(user_id + account_id + folder).  
- **logout_behaviour**: Closes all connections, deletes user cache folder.  
- **multi_account**: `getAccounts()` returns PB array; active account via `accountId`.  

### Sweep 2: World-class Abilities (5 fields)
- **connection_pool**: Per-account `ImapClient`/`SmtpClient` instances with 60s TTL.  
- **offline_cache**: `getMessageList()` merges fresh + cached data with priority flags.  
- **sieve_integration**: Sieve scripts mirrored in PB `email_sieve_rules`.  
- **audit_logging**: Every facade method logs to PB with timestamp/operation/status.  
- **ux_primitives**: `getInbox()` returns paginated data with `has_more` cursor.  

### Sweep 3: Migration + Observability + Tests (3 fields)
- **migration**: `migrateFromSnappyMail()` scans filesystem, imports accounts, sets up cache.  
- **observability**: RED metrics logged to PB `email_metrics` with method/duration/error tags.  
- **tests**: Greenmail for IMAP/SMTP mock; PHPUnit test suite with 8 test cases.  

## 16. Cross-references
- §11: Constitutional rules for module design  
- §37: Public contract and substitutability  
- §38: Formation intent specification  
- §19: Clean-room extraction methodology  
- Dependent: `ui-bridge` (inbox data consumer), `notification-service` (email trigger)  

## 17. What we are intentionally NOT doing (deferred to future)
- **HTML Email Rendering**: Defer to React UI library  
- **Calendar Integration**: Separate `mail-calendar` module  
- **Advanced Sieve Scripting**: Support basic rules only  
- **Password Recovery**: Handled by PB auth module  
- **Mailbox Search**: Add in v1.1 (current: folder-only)  
- **Chat/Messaging**: Outside email scope  
- **Encryption End-to-End**: Out of scope for v1.0  
- **POP3 Protocol**: IMAP only  
- **Complex Message Threading**: Simple "reply-to" links  
- **Email Provider Onboarding**: Manual PB configuration only  
- **File Compositors**: No `AttachmentCollection` aggregation  
- **Message Classification**: External ML service integration  
- **Complex Folders**: Subfolders supported, no custom flags