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

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

---

## 1. Intent (the "why")
- **Why it exists**: FreshCloud Mail requires a decoupled, multi-tenant IMAP client that securely bridges multiple user accounts with remote mail servers while supporting offline workflows and audit compliance.  
- **Problem it solves**: Eliminates server-specific protocol quirks, replaces deprecated AGPL-licensed code with MIT-licensed implementation, and provides unified authentication across IMAP/SMTP accounts.  
- **Role in system**: Acts as the protocol engine between FreshCloud's UI (React) and mail servers (Gmail, Outlook, etc.), managing connection lifecycle, message retrieval, folder operations, and local caching.  
- **Defining principle**: "User-Scoped Session Isolation" - every operation must be explicitly tied to a (user_id, account_id) pair with zero cross-contamination between users or accounts.

---

## 2. User-facing behaviour
| User Action | Module Response |
|-------------|----------------|
| Connect to mailbox | Establishes TLS connection, authenticates, returns connection token |
| List folders | Returns array of Folder objects with name, unread count, and UID validity |
| Fetch messages | Returns paginated Message objects with headers, flags, and body previews |
| Set message flags | Updates \Seen/\Answered flags on server and emits audit event |
| Search messages | Executes IMAP SEARCH command with advanced query support |
| Move message | Copies then deletes (IMAP MOVE extension) and updates local cache |
| Enable offline mode | Triggers full folder sync to encrypted local cache |
| View audit log | Returns command history for specific account connection |

---

## 3. Public contract (data shapes + signatures)
```typescript
// FreshCloud\Mail\ImapClient\*
interface ImapClientInterface {
  connect(user_id: int, account_id: int): ConnectionResult;
  disconnect(): void;
  selectFolder(folderName: string): FolderInfo;
  getFolderList(): FolderCollection;
  fetchMessages(folder: string, page: int, limit: int): MessageCollection;
  searchMessages(criteria: SearchCriteria): MessageCollection;
  setFlags(messageUids: string[], flags: MessageFlag[]): AuditResult;
  getThread(rootMessageId: string): MessageThread;
  enableOfflineMode(): void;
  getAuditLog(accountId: int): AuditEntry[];
}

interface ConnectionResult {
  success: boolean;
  connectionId: string;
  serverCapabilities: string[];
  error?: string;
}

interface FolderInfo {
  name: string;
  uidValidity: string;
  unreadCount: int;
  totalMessages: int;
  isSelectable: boolean;
}

interface MessageCollection {
  messages: Message[];
  pagination: {
    hasMore: boolean;
    nextPageToken: string;
  };
  stale: boolean; // Indicates if cached data
}

interface SearchCriteria {
  keywords: string[];
  from?: string;
  dateRange?: [Date, Date];
  hasAttachments?: boolean;
  isUnread?: boolean;
}

interface Message {
  uid: string;
  messageId: string;
  subject: string;
  from: EmailAddress;
  date: Date;
  hasAttachment: boolean;
  flags: MessageFlag[];
  bodyPreview: string; // First 200 chars of decoded body
}

interface AuditEntry {
  timestamp: Date;
  command: string;
  status: 'success' | 'failed';
  details: {
    folder?: string;
    messageCount?: int;
    error?: string;
  };
}
```

---

## 4. Persistence (what gets stored where)
| Data Source | Storage Target | Schema |
|-------------|----------------|--------|
| Account credentials | PocketBase `email_accounts` | user_id (FK), account_id (PK), imap_host, imap_port, imap_username, imap_password_enc |
| Connection state | Memory-only | Connection pool keyed by (user_id, account_id) |
| Audit logs | PocketBase `email_audit` | id (PK), user_id, account_id, timestamp, command, status, details (JSON) |
| Message cache | Encrypted filesystem | User-specific cache files: `user_{user_id}/account_{account_id}/folders.json`, `messages_uid.json` |
| Sieve scripts | IMAP server | STORED on server via MANAGESIEVE |
| Offline sync status | Local metadata | `last_sync_at` in cache metadata |

---

## 5. Dependencies (what this module uses, what uses this)
| Dependency Type | Name | Purpose |
|-----------------|------|---------|
| Engine Module | `socket-client` | TLS socket connections |
| Engine Module | `crypto` | Encryption for local cache |
| Engine Module | `logger` | Structured logging |
| External | PocketBase | Credentials storage and user auth boundary |
| App Module | `mail-store` | Message persistence (future) |
| App Module | `auth-service` | User session validation |
| Test Framework | Greenmail | Mock IMAP server for tests |

---

## 6. Behaviour inventory (the N public methods)
1. **`connect(user_id, account_id)`**  
   - Signature: `(int, int): ConnectionResult`  
   - Purpose: Establishes authenticated TLS connection to IMAP server.  
   - Side Effects: Creates connection pool entry, writes initial audit log.  
   - Errors: `ConnectionException` (network), `AuthException` (credentials).  

2. **`selectFolder(folderName)`**  
   - Signature: `(string): FolderInfo`  
   - Purpose: Activates mailbox for message operations.  
   - Side Effects: Updates state machine, updates cache metadata.  
   - Errors: `NoSuchFolderException`.  

3. **`getFolderList()`**  
   - Signature: `(): FolderCollection`  
   - Purpose: Returns all mailboxes with status counts.  
   - Side Effects: Populates local cache if stale.  
   - Errors: `ServerErrorException`.  

4. **`fetchMessages(folder, page, limit)`**  
   - Signature: `(string, int, int): MessageCollection`  
   - Purpose: Retrieves paginated message list with previews.  
   - Side Effects: Updates message cache.  
   - Errors: `QuotaExceededException`.  

5. **`searchMessages(criteria)`**  
   - Signature: `(SearchCriteria): MessageCollection`  
   - Purpose: Executes IMAP SEARCH with advanced filters.  
   - Side Effects: None (read-only).  
   - Errors: `InvalidQueryException`.  

6. **`setFlags(messageUids, flags)`**  
   - Signature: `(string[], MessageFlag[]): AuditResult`  
   - Purpose: Updates message flags (e.g., mark as read).  
   - Side Effects: Updates server and local cache.  
   - Errors: `PermissionDeniedException`.  

---

## 7. State machine
| State | Allowed Transitions | Trigger |
|-------|---------------------|---------|
| `DISCONNECTED` | → CONNECTING | `connect()` called |
| `CONNECTING` | → AUTHENTICATED (success) | Server greeting received |
| | → DISCONNECTED (failure) | Connection timeout |
| `AUTHENTICATED` | → SELECTED | `selectFolder()` success |
| | → DISCONNECTED | `disconnect()` or timeout |
| `SELECTED` | → AUTHENTICATED | Folder deselected |
| | → DISCONNECTED | Connection lost |
| Any State | → STALE CACHE | Network timeout during fetch |

---

## 8. Side effects
| Effect | Trigger | Target |
|--------|---------|--------|
| Audit Log Write | connect, disconnect, setFlags, search | `email_audit` PB table |
| Local Cache Write | fetchMessages, getFolderList | Encrypted FS cache |
| Cache Invalidation | IMAP EXPUNGE command | Local cache files |
| Pool Entry Cleanup | Disconnect | Memory (connection pool) |
| Sieve Upload | rule-save | IMAP server via MANAGESIEVE |
| Offline Sync Trigger | enableOfflineMode | Local cache metadata |

---

## 9. Input validation
| Parameter | Validation Rule | Error Type |
|-----------|-----------------|------------|
| `user_id` | Must match active session | `InvalidSessionException` |
| `account_id` | Must belong to user | `UnauthorizedAccountException` |
| `folderName` | Must match IMAP LIST syntax | `InvalidFolderException` |
| `messageUids` | Must be valid IMAP sequence set | `InvalidUidException` |
| `searchCriteria` | Keywords < 256 chars, dates in range | `MalformedQueryException` |
| `flags` | Must be in [\Seen, \Answered, \Flagged, \Draft] | `InvalidFlagException` |

---

## 10. Failure modes
| Failure | Module Response | User Experience |
|---------|----------------|-----------------|
| Network timeout | Auto-reconnect twice, then return stale cache | "Offline mode - showing last synced data" |
| Auth failure | Drop connection, log to PB, clear cache | "Please re-enter credentials" |
| Server 401 error | Terminate connection, emit audit event | Login page with retry button |
| Quota exceeded | Fail with HTTP 429 equivalent | "Storage full" message |
| Folder renamed by another client | Rebuild folder list cache | UI refreshes folder list |

---

## 11. User simulation
1. **Connect**  
   Given: User has Gmail account in PB  
   When: Calls connect(user_id, 42)  
   Then: Returns authenticated connection, logs to `email_audit`  

2. **View Inbox**  
   Given: User connected to "INBOX"  
   When: Calls fetchMessages("INBOX", 1, 10)  
   Then: Returns first 10 messages with unread flags  

3. **Mark as Read**  
   Given: Message with uid=123 is unread  
   When: Calls setFlags([123], [\Seen])  
   Then: Updates flag in server and cache, emits audit event  

4. **Search Sent Items**  
   Given: User wants "invoice" emails  
   When: Calls searchMessages({keywords: ["invoice"]})  
   Then: Returns filtered messages from Sent folder  

5. **Offline Mode**  
   Given: Network unavailable  
   When: Calls enableOfflineMode()  
   Then: Syncs all messages to cache, UI shows "Offline" indicator  

6. **Logout**  
   Given: User clicks "Sign Out"  
   When: System closes all user connections  
   Then: Pool entries cleared, cache marked stale  

---

## 12. Reproducibility test
1. Install PocketBase with freshmail schema  
2. Create user 42, account 42 in `email_accounts`  
3. Deploy Greenmail mock server with test credentials  
4. Run `composer require pocketbase/greenmail`  
5. Call `ImapClientFactory::create()->connect(42, 42)`  
6. Verify connection returns 200 OK with capabilities  
7. Call `selectFolder("INBOX")` and validate folder data  
8. Execute `setFlags([1], [\Seen])` and confirm audit log entry  
9. Disconnect and verify pool entry cleanup  

---

## 13. Substitutability (FvW v8 §37.4)
- **Interface requirement**: Must implement `ImapClientInterface` with identical signatures  
- **State machine requirement**: Must support DISCONNECTED→CONNECTING→AUTHENTICATED→SELECTED transitions  
- **Scoping requirement**: All methods require (user_id, account_id) as first parameters  
- **Cache requirement**: Must return `stale: true` when serving cached data  
- **Audit requirement**: Must write structured logs to `email_audit` PB table on every state change  

---

## 14. Invariants (the 5 things that must always hold)
1. **Zero-Knowledge Credentials**: Module never decrypts PB passwords; receives encrypted blob from auth-service  
2. **Connection Pool Isolation**: Connections for different users never share state  
3. **Cache Staleness**: All cache writes are atomic; partial syncs never corrupt data  
4. **Audit Trail**: Every connection auth, disconnect, and flag change emits an audit record  
5. **Clean-Screen Protocol**: No AGPLv3 code in implementation (MIT-only lineage)  

---

## 15. Sweep fields applied (the 14 design intent fields)
### Sweep 1: Multi-user + PocketBase (6 fields)
- **user_scoping**: All methods take `(user_id, account_id)`; internal state keyed by pair  
- **pb_integration**: Reads `email_accounts` for credentials; writes `email_audit` for trails  
- **auth_boundary**: Decrypts credentials externally; never accesses PB tables directly  
- **state_isolation**: Connection pools per user; local cache files scoped by `user_id/`  
- **logout_behaviour**: Clear all connections for user_id; mark cache stale  
- **multi_account**: One connection per (user_id, account_id); N parallel connections per user  

### Sweep 2: World-class Abilities (5 fields)
- **connection_pool**: ConnectionPool class with maxConcurrentPerUser config  
- **offline_cache**: Encrypted FS cache; sync triggered by `enableOfflineMode()`  
- **sieve_integration**: SIEVE scripts stored on server; MANAGESIEVE sub-connection for upload  
- **audit_logging**: Every command emits `email_audit` record with {command, status, user_id}  
- **ux_primitives**: `getUnreadCount()`, `getThread()`, `searchMessages()` return UI-ready data  

### Sweep 3: Migration + Observability + Tests (3 fields)
- **migration**: Import SnappyMail `accounts.json` to PB `email_accounts`; preserve encrypted passwords  
- **observability**: Metrics: `imap_connection_duration_seconds` (histogram), `imap_auth_failures_total`  
- **tests**: PHPUnit tests against Greenmail; mock IMAP scenarios for all failure modes  

---

## 16. Cross-references
- §11: FreshVibe Way v8 Architecture Principles (Module Isolation)  
- §19: Recipe Extraction Process (MailSo as reference)  
- §37: Public Contract (Substitutability Requirements)  
- §38: Formation Phase (This Document)  
- Dependencies: `auth-service` (decryption), `socket-client` (TLS), `crypto` (cache encryption)  
- Dependent Modules: `mail-store` (future), `ui-bridge` (interface contract)  

---

## 17. What we are intentionally NOT doing (deferred to future)
1. ~~MailSo's complex MIME parsing~~ (Handled by dedicated `mime-parser` module)  
2. ~~Smtp client functionality~~ (Separate `smtp-client` module)  
3. ~~IMAP IDLE for real-time updates~~ (WebSocket events handled by mail-bridge)  
4. ~~Calendar/Sync extensions~~ (Future `ical-parser` module)  
5. ~~Retroactive mailbox migration~~ (Handled by migration tool, not client)  
6. ~~Server capability negotiation~~ (Fixed to IMAP4rev1 + Gmail extensions)  
7. ~~Message threading algorithms~~ (Server-side THREAD=REFS command only)  
8. ~~Network failover proxies~~ (Handled by infrastructure layer)  
9. ~~Quota management~~ (Deferred to future `mail-quota` module)  
10. ~~DKIM signature verification~~ (Future `mail-dkim` module)  
11. ~~MailSo's 22 PHP files~~ (Reduced to 6 core classes + interfaces)  
12. ~~IMAP COMPRESS extension~~ (TLS compression disabled for simplicity)  
13. ~~Cross-server copy operations~~ (Client handles only per-account operations)  
14. ~~Automatic account discovery~~ (Pure manual PB configuration)