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

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

---

## 1. Intent (the "why")
FreshCloud needs a sophisticated, user-centric request builder for IMAP search, sort, and threading operations. This module translates user interface interactions into optimized IMAP commands (ESearch, SORT, THREAD) while respecting FreshCloud's multi-tenant architecture. It solves the problem of complex mail filtering without exposing users to raw IMAP syntax and enables rich email search experiences across multiple accounts. It serves as the query engine for the mail system's data plane, feeding results to the UI layer for display.

## 2. User-facing behaviour
- **Search**: Users build criteria via "From", "Subject", "Date Range" filters that get translated to ESearch
- **Sort**: Messages can be sorted by Date, From, Subject, and custom criteria
- **Thread**: Conversations grouped by message threads (via THREAD extension)
- **Pagination**: Results returned in paginated chunks with "has_more" flags
- **Facets**: Search metadata (sender counts, date ranges) returned for UI filter chips
- **Multi-account**: Requests scoped to selected account

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

interface QueryCriteria {
  from?: string;
  to?: string;
  subject?: string;
  date_range?: [Date, Date];
  body?: string;
  flags?: string[];
}

interface SortCriteria {
  field: 'date' | 'from' | 'to' | 'subject' | 'size';
  order: 'asc' | 'desc';
}

interface Pagination {
  limit: number;
  offset: number;
}

class SearchResult {
  messages: MessageRef[];
  total: number;
  has_more: boolean;
  facets?: {
    senders: { email: string; count: number }[];
    date_range?: [Date, Date];
  };
}

interface MessageRef {
  uid: number;
  subject: string;
  from: string;
  date: Date;
  size: number;
  thread_id: string;
}

interface ImapRequests {
  search(
    criteria: QueryCriteria, 
    pagination: Pagination,
    accountId: string
  ): Promise<SearchResult>;
  
  sort(
    sort: SortCriteria,
    pagination: Pagination,
    accountId: string
  ): Promise<SearchResult>;
  
  thread(
    criteria: QueryCriteria,
    pagination: Pagination,
    accountId: string
  ): Promise<SearchResult>;
}
```

## 4. Persistence (what gets stored where)
- **PocketBase Tables**:
  - `email_audit` (existing): Logs all search queries with:
    - `query_hash` (string, unique)
    - `user_id` (foreign key)
    - `account_id` (foreign key)
    - `criteria_json` (text)
    - `result_count` (int)
    - `duration_ms` (int)
    - `timestamp` (datetime)
- **Local Cache**:
  - Encrypted search results cache (file system)
  - Cache key format: `sha256(user_id + account_id + query_hash)`
  - TTL: Configurable (default 24h)
  - Structure: `{ results: MessageRef[], expires_at: timestamp }`
- **No new database tables needed**

## 5. Dependencies (what this module uses, what uses this)
- **Uses**:
  - `core-utils` (string validation, date formatting)
  - `imap-client` (IMAP protocol implementation)
  - `crypto-key-manager` (for cache encryption)
- **Used by**:
  - `imap-commands` (command orchestration)
  - `ui-mail-search` (React search interface)
  - `account-sync` (background indexing)

## 6. Behaviour inventory (the N public methods)

| Method       | Signature | Purpose | Side Effects | Errors |
|--------------|-----------|---------|--------------|--------|
| `search`     | `QueryCriteria, Pagination, string → Promise<SearchResult>` | ESearch for messages | Caches results, writes audit log | InvalidCriteriaException, QuotaExceededError |
| `sort`       | `SortCriteria, Pagination, string → Promise<SearchResult>` | SORT command execution | Writes audit log | InvalidSortException, ServerError |
| `thread`     | `QueryCriteria, Pagination, string → Promise<SearchResult>` | THREAD command execution | Caches results, writes audit log | ThreadingNotSupportedError, EmptyResultException |

## 7. State machine
```
[INIT] → 
  [BUILDING] (when called) → 
    [VALIDATING] (checks criteria) → 
      [CACHING] (checks cache) → 
        [EXECUTING] (runs IMAP command) → 
          [LOGGING] (writes audit) → 
            [DONE] (returns result)
        ↓
      [CACHE_HIT] (returns cached result)
        ↓
    [VALIDATION_FAILED] → throws InvalidCriteriaException
    ↓
  [SERVER_ERROR] → throws ImapException
```

## 8. Side effects
- **Writes**:
  - Audit records to `email_audit` table
  - Search result cache to encrypted local files
- **Reads**:
  - User's IMAP account via `imap-client`
  - Existing search cache
  - Account configuration (quota limits)
- **Emits**:
  - `search.completed` event with result metrics
  - `cache.miss/hit` events

## 9. Input validation
- Criteria must contain at least one non-empty field
- Date range must be valid (end ≥ start)
- Sort fields limited to predefined list
- Pagination: limit ≤ 100 (configurable), offset ≥ 0
- Account ID must match user's configured accounts
- User ID must be present (via auth context)

## 10. Failure modes
| Failure | Handler | Caller Sees |
|---------|---------|-------------|
| IMAP timeout | Retry once with exponential backoff | TimeoutException after 5s |
| Invalid criteria | Immediate rejection | `{"error": "invalid_criteria", "details": "missing_from"}` |
| Quota exceeded | Abort audit write | `{"error": "quota_exceeded"}` |
| Cache decryption fail | Force fresh fetch | No error (just miss cache) |
| Server error | Log and retry 3x | ServerException with attempt count |

## 11. User simulation
**Scenario 1: Basic Search**  
*Given* User has 500 messages in "Inbox"  
*When* User searches for "urgent" in subject  
*Then* Returns top 50 matches with "has_more: true"

**Scenario 2: Threaded Search**  
*Given* User has 30 messages in thread  
*When* User sorts by "date" ascending  
*Then* Groups messages by thread_id, sorted chronologically

**Scenario 3: Empty Results**  
*Given* No messages match "tax refund"  
*When* User performs search  
*Then* Returns empty array with facets showing date ranges

**Scenario 4: Multi-Account**  
*Given* User has 2 accounts (Work/Home)  
*When* User searches in Work account for "project x"  
*Then* Only searches Work mailbox, returns account-specific results

**Scenario 5: Facet Generation**  
*Given* Search across 200 messages  
*When* User searches with no filters  
*Then* Returns facet counts for top 10 senders and date range

## 12. Reproducibility test
1. Create user account with 100 test emails in Greenmail
2. Configure IMAP credentials for test account
3. Execute search with criteria: `from: "boss@example.com"`
4. Verify returned results contain 5 messages
5. Check audit log contains query record
6. Execute same query again
7. Verify cache hit (second request faster)
8. Execute sort by date descending
9. Verify messages sorted newest-first with thread IDs

## 13. Substitutability (FvW v8 §37.4)
A drop-in alternative must provide:
- Exact method signatures for `search()`, `sort()`, `thread()`
- Identical `SearchResult` object structure
- Audit logging with same schema
- Cache key compatibility (same hash algorithm)
- Pagination and facet support
- Multi-account scoping via accountId parameter

## 14. Invariants (the 5 things that must always hold)
1. All requests scoped to exactly one user and account
2. Audit records contain: query_hash, user_id, account_id, result_count, duration
3. Cache keys use: sha256(user_id + account_id + query_string)
4. Sort orders must be explicit (asc/desc)
5. Thread IDs are stable across queries (based on Message-ID headers)

## 15. Sweep fields applied (the 14 design intent fields)

### Sweep 1: Multi-user + PocketBase
- **user_scoping**:  
  *Source:* Sweep 1  
  *Intent:* Every operation scoped to authenticated user  
  *Implementation:* Methods require accountId parameter, injected via auth middleware
  
- **pb_integration**:  
  *Source:* Sweep 1  
  *Intent:* Audit all search operations  
  *Implementation:* writeToAudit() method writing to existing `email_audit` table
  
- **auth_boundary**:  
  *Source:* Sweep 1  
  *Intent:* No separate auth; inherits from ImapClient  
  *Implementation:* Dependency injection of authenticated ImapClient instance
  
- **state_isolation**:  
  *Source:* Sweep 1  
  *Intent:* No shared state between users  
  *Implementation:* Class-level statelessness, all state passed via parameters
  
- **logout_behaviour**:  
  *Source:* Sweep 1  
  *Intent:* Cache cleared on logout  
  *Implementation:* Cache invalidated via logout event listener
  
- **multi_account**:  
  *Source:* Sweep 1  
  *Intent:* Support multiple IMAP accounts per user  
  *Implementation:* accountId parameter in all public methods

### Sweep 2: World-class Abilities
- **connection_pool**:  
  *Source:* Sweep 2  
  *Intent:* Efficient connection reuse  
  *Implementation:* Injects shared connection pool from imap-client module
  
- **offline_cache**:  
  *Source:* Sweep 2  
  *Intent:* Serve results when network unavailable  
  *Implementation:* Local file cache with configurable TTL
  
- **sieve_integration**:  
  *Source:* Sweep 2  
  *Intent:* Not applicable for read operations  
  *Implementation:* Disabled flag in class constants
  
- **audit_logging**:  
  *Source:* Sweep 2  
  *Intent:* Track all search operations  
  *Implementation:* Audit record creation before returning results
  
- **ux_primitives**:  
  *Source:* Sweep 2  
  *Intent:* Rich metadata for UI rendering  
  *Implementation:* Facets object in SearchResult with sender/date metadata

### Sweep 3: Migration + Observability + Tests
- **migration**:  
  *Source:* Sweep 3  
  *Intent:* Zero migration needed  
  *Implementation:* IMAP RFC compliance ensures backward compatibility
  
- **observability**:  
  *Source:* Sweep 3  
  *Intent:* Track performance and errors  
  *Implementation:* Metrics: search_duration_histogram, result_count_histogram
  
- **tests**:  
  *Source:* Sweep 3  
  *Intent:* Full coverage of IMAP scenarios  
  *Implementation:* PHPUnit tests with Greenmail for all request types

## 16. Cross-references
- FvW v8 §38: Formation Recipe structure
- FvW v8 §37: Fragment requirements (must implement substitutable interface)
- FvW v8 §11: Constitutional rules (11.9 module structure)
- FvW v8 §19: MailSo as reference only (no direct code reuse)
- Dependencies: `imap-client` (command execution), `core-utils` (validation)
- Consumed by: `ui-mail-search` (React interface)

## 17. What we are intentionally NOT doing (deferred to future)
- [ ] Full-text search in message bodies (limited to basic substring for now)
- [ ] Advanced regex parsing in search criteria
- [ ] Server-side rule execution (Sieve-like filtering)
- [ ] Search result streaming (all results loaded at once)
- [ ] IMAP UID FETCH batch optimization (single UID fetch per query)
- [ [ ] Search history persistence (audit covers queries but not criteria replay)
- [ ] Real-time search updates (push notifications for new matches)
- [ ] Complex date math (e.g., "older than 2 weeks")
- [ [ ] Threading priority scoring (current simple thread grouping)
- [ ] Cross-folder search (single-folder only)
- [ [ ] MailSo had 7 PHP files. We have 4 (simpler architecture, no separate THREAD/SORT/ESEARCH classes)