# sieve — Formation Recipe (FvW v8 §38)

**Module ID:** `sieve`
**Module name:** Sieve Rule Manager
**Lifecycle phase:** formation
**Status:** INTENT ONLY. No code yet.
**Date:** 2026-07-21

---

## 1. Intent (the "why")
FreshCloud Mail requires a decentralized email filtering system that operates entirely on the mail server. This Sieve module enables users to create complex rules for automatically organizing, forwarding, or deleting emails before they reach the client. It solves the core problem of inbox bloat and improves user productivity by shifting heavy filtering logic away from client devices. The module serves as the translation layer between user-friendly rule definitions and RFC 5228-compliant Sieve scripts, while maintaining strict multi-user isolation and real-time synchronization between server-side execution and our PocketBase audit trail.

## 2. User-facing behaviour
Users interact with Sieve rules through three core operations:  
- **Rule Definition**: Create/edit scripts via web UI with natural language conditions (e.g., "If sender contains 'news@company.com', move to 'News' folder")  
- **Script Management**: Activate/deactivate rules, rename scripts, and toggle script status via server commands  
- **Execution Control**: Trigger scripts manually for mailbox maintenance  
The module exposes a stateful interface where changes persist immediately on the server, with the UI reflecting rule status in real-time.

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

interface SieveClient {
    // Script management
    public function listScripts(): array<ScriptMetadata>;
    public function getScript(string $name): ScriptContent;
    public function putScript(string $name, string $script): void;
    public function deleteScript(string $name): void;
    
    // Execution control
    public function setActiveScript(string $name): void;
    public function getActiveScript(): string;
    public function runScript(string $name): ExecutionResult;
    
    // UI primitives
    public function getRulesUI(): array<UIRule>;
    public function validateScript(string $script): ValidationResult;
}

// Shape definitions
type ScriptMetadata = {
    id: string,
    name: string,
    enabled: bool,
    lastModified: DateTimeImmutable
};

type ScriptContent = {
    name: string,
    body: string,
    serverHash: string
};

type UIRule = {
    id: string,
    name: string,
    enabled: bool,
    conditions: array<Condition>,
    actions: array<Action>
};

type Condition = {
    field: 'sender' | 'recipient' | 'subject' | 'body',
    operator: 'contains' | 'not_contains' | 'equals' | 'matches',
    value: string
};

type Action = {
    type: 'move' | 'forward' | 'discard' | 'flag',
    params: array
};

type ValidationResult = {
    valid: bool,
    errors: array<ValidationError>,
    serverHash?: string
};

type ExecutionResult = {
    success: bool,
    changes: array<EmailChange>,
    duration: float
};
```

## 4. Persistence (what gets stored where)
- **PocketBase Storage**:  
  `email_sieve_rules` table:  
  ```json
  {
    "id": "unique_id",
    "user_id": "auth_user.id",
    "account_id": "linked_account.id",
    "name": "Newsletter Filter",
    "script": "if header :contains \"List-ID\" \"mailchimp\" { fileinto \"Newsletters\"; }",
    "enabled": true,
    "last_modified": "2026-07-21T12:34:56Z",
    "server_version": "v3"
  }
  ```
- **Audit Trail**:  
  `email_audit` table logs:  
  ```json
  {
    "type": "sieve_script_modification",
    "user_id": "...",
    "account_id": "...",
    "script_name": "...",
    "old_hash": "...",
    "new_hash": "...",
    "timestamp": "..."
  }
  ```
- **Local Cache**:  
  `offline_cache/sieve/<user_id>_<account_id>.json` for browser storage

## 5. Dependencies (what this module uses, what uses this)
- **Engine Modules**:  
  `auth` (user context), `net` (MANAGESIEVE connection), `pocketbase` (data persistence), `log` (audit events)  
- **External Libraries**:  
  `php-imap` (IMAP extension for MANAGESIEVE), `psr/log` (event logging)  
- **Consumers**:  
  UI Bridge (React hooks), Automation Engine (script execution triggers), Data Sync Module (PB synchronization)

## 6. Behaviour inventory (the N public methods)
| Method | Signature | Purpose | Side Effects | Errors |
|--------|----------|---------|--------------|--------|
| `listScripts` | `(): array<ScriptMetadata>` | Retrieve all user scripts | None | `AuthenticationException`, `ConnectionException` |
| `getScript` | `(string name): ScriptContent` | Fetch script content and server hash | Cache update | `ScriptNotFoundException` |
| `putScript` | `(string name, string script): void` | Upload new/modified script | PB write, audit log, cache invalidation | `ValidationException`, `ServerException` |
| `deleteScript` | `(string name): void` | Remove script from server | PB delete, audit log | `ScriptNotFoundException` |
| `setActiveScript` | `(string name): void` | Mark script as server-active | PB update, audit log | `ScriptNotFoundException` |
| `runScript` | `(string name): ExecutionResult` | Manually trigger script | Server execution, PB update | `ExecutionException` |
| `getRulesUI` | `(): array<UIRule>` | Convert Sieve to UI-friendly format | None | `ConversionException` |

## 7. State machine
```mermaid
stateDiagram-v2
    [*] --> Idle
    Idle --> Connecting : connect()
    Connecting --> Ready : Auth success
    Connecting --> Error : Auth failure
    Ready --> Listing : listScripts()
    Ready --> Fetching : getScript()
    Ready --> Uploading : putScript()
    Ready --> Activating : setActiveScript()
    Ready --> Running : runScript()
    Ready --> Idle : disconnect()
    Listing --> Ready : Return list
    Fetching --> Ready : Return content
    Uploading --> Ready : Store script
    Activating --> Ready : Update active state
    Running --> Ready : Execute script
    Error --> Idle : Reset on disconnect
```

## 8. Side effects
- **Writes**:  
  - `email_sieve_rules` (PB) on script/activation changes  
  - `email_audit` (PB) for all modifications  
  - Local cache (FS) for script content and metadata  
  - Server MANAGESIEVE storage for script activation  
- **Reads**:  
  - IMAP server capabilities (CAPABILITY)  
  - Server script storage (GETSCRIPT/LISTSCRIPTS)  
  - PB `email_sieve_rules` for metadata sync  
- **Emits**:  
  - `ScriptModified` event (to UI and automation engine)  
  - `AuditTrailUpdated` event (to compliance module)

## 9. Input validation
- **Script Names**: 3-64 chars, alphanumeric + hyphen/underscore, no path separators  
- **Script Body**: Max 50KB, Sieve RFC 5228 syntax validation during upload  
- **Activation**: Script must exist and be valid before activation  
- **User Context**: Requires authenticated `user_id` and `account_id` on all operations  
- **Rate Limiting**: Max 30 script updates per user per minute

## 10. Failure modes
| Failure | Handling | Exception |
|---------|----------|----------|
| MANAGESIEVE unavailable | Fall back to local mode, log error | `ServerUnavailableException` |
| Script syntax error | Reject upload with line number details | `ValidationException` |
| Authentication failure | Clear credentials, prompt re-login | `AuthenticationException` |
| Network timeout | Retry 3 times then fail | `ConnectionException` |
| PB write conflict | Trigger automatic retry | `ConcurrencyException` |
| Server execution fail | Return execution error details | `ExecutionException` |

## 11. User simulation
**Scenario 1: New Rule Creation**  
Given: User is on Settings/Email Rules page  
When: User creates "Vacation Auto-Reply" rule  
Then: Script stored in PB, server activation set, audit log created  

**Scenario 2: Manual Execution**  
Given: "Spam Filter" exists and is enabled  
When: User runs "Spam Filter" manually  
Then: Server executes script, shows processed emails, logs execution metrics  

**Scenario 3: Offline Mode**  
Given: Network unavailable, cached rules exist  
When: User accesses rules in browser  
Then: UI shows cached rules, marks as "Offline Editing"  

**Scenario 4: Multi-Account Isolation**  
Given: User has two email accounts  
When: Creates "Work Filter" in Account A  
Then: Rule not visible in Account B's namespace  

**Scenario 5: Audit Trail Access**  
Given: Compliance officer queries email_audit  
When: Filters sieve_script_modification events  
Then: Shows script diff, user ID, timestamps  

**Scenario 6: Migration Import**  
Given: User imports SnappyMail Sieve export  
When: Parsed rule contains invalid syntax  
Then: Shows error with specific Sieve line, prevents upload  

## 12. Reproducibility test
1. Install PHP 8.0+ with IMAP extension  
2. Configure PocketBase with `email_sieve_rules` schema  
3. Create test user and dummy IMAP account  
4. Implement SieveClient class skeleton  
5. Write `listScripts()` method connecting via MANAGESIEVE  
6. Create `getRulesUI()` with basic condition/action mapping  
7. Implement PB sync for `putScript()` with audit logging  
8. Add script validation using RFC 5228 grammar  
9. Build UI bridge component showing rule status  

## 13. Substitutability (FvW v8 §37.4)
Any alternative implementation must provide:  
- SieveClient class with identical method signatures  
- Same UIRule data shape and conversion logic  
- PB schema compliance for `email_sieve_rules`  
- Audit trail integration via `email_audit` table  
- Offline caching to FS using `<user_id>_<account_id>` pattern  
- Error hierarchy matching defined exceptions  

## 14. Invariants (the 5 things that must always hold)
1. **Server-Primacy**: All scripts execute on IMAP server, never client-side  
2. **User Isolation**: Script access strictly scoped to `(user_id, account_id)` pairs  
3. **PB Truth**: `email_sieve_rules` is the authoritative script source  
4. **Audit Completeness**: Every script modification logged in `email_audit`  
5. **Protocol Compliance**: All scripts validated against RFC 5228 standard  

## 15. Sweep fields applied (the 14 design intent fields)
### Sweep 1: Multi-user + PocketBase
- **user_scoping**:  
  Design: All operations scoped to `(user_id, account_id)`  
  PHP: Class constructor injects auth context, all methods validate permissions  
- **pb_integration**:  
  Design: Real-time mirror with PB as source of truth  
  PHP: `putScript()` writes to PB before server sync  
- **auth_boundary**:  
  Design: IMAP credentials as auth source  
  PHP: Uses `Net\Auth` module for connection handling  
- **state_isolation**:  
  Design: Strict multi-user isolation  
  PHP: PB row-level security enforced at schema level  
- **logout_behaviour**:  
  Design: No server action on logout  
  PHP: Connection manager handles persistent sessions  
- **multi_account**:  
  Design: Per-account script namespaces  
  PHP: `account_id` in all PB table operations  

### Sweep 2: World-class Abilities
- **connection_pool**:
  Design: N/A — Sieve executes over the existing IMAP connection (per RFC 5804 §1.1). The imap-client module owns the pool.
  PHP: No new code; documentation note in `SIEVE.md` and method docblock on `SieveClient::__construct()`.
- **offline_cache**:  
  Design: Browser-accessible script cache  
  PHP: `getRulesUI()` reads from FS cache when network down  
- **sieve_integration**:  
  Design: Full RFC 5228 protocol implementation  
  PHP: Dedicated SieveParser class with grammar validation  
- **audit_logging**:  
  Design: Comprehensive change tracking  
  PHP: Audit logger injected into all state-changing methods  
- **ux_primitives**:  
  Design: UI-native rule representation  
  PHP: `UIRule` shape conversion in `getRulesUI()`  

### Sweep 3: Migration + Observability + Tests
- **migration**:  
  Design: SnappyMail to FreshCloud script import  
  PHP: `parseSnappySieve()` method in import service  
- **observability**:  
  Design: Sieve execution metrics  
  PHP: Prometheus `sieve_execution_duration_seconds` counter  
- **tests**:  
  Design: Sieve protocol and error scenario testing  
  PHP: PHPUnit suite with mock MANAGESIEVE server  

## 16. Cross-references
- **FvW v8**: §11 (Rule Management), §19 (Clean-Room Reconstruction), §37 (Substitutability), §38 (Formation Lifecycle)  
- **Modules**: `auth` (user context), `net` (IMAP connection), `pocketbase` (PB sync), `log` (audit emission)  
- **Documents**: `recipes/engine/log/formation.md` (logging pattern), `snappymail-migration-v2.0.pdf` (import schema)  

## 17. What we are intentionally NOT doing (deferred to future)
1. Server-side script editing interface (deferred to v2)  
2. Performance optimization for >100 scripts per user  
3. Advanced Sieve extensions (e.g., regex with PCRE)  
4. Script execution scheduling (only manual trigger)  
5. Cross-account script sharing  
6. Sieve script templates library  
7. Rule visualization/graph view  
8. Server-side script validation UI feedback  
9. Bulk script operations (batch delete/enable)  
10. Sieve script versioning (PB mirrors single latest version)  
11. MailSo's 7-file class structure (we use 4: SieveClient, Parser, Validator, Exceptions)  
12. Direct DB access (all through PocketBase API)  
13. Real-time script execution status (deferred to WebSocket v2)  
14. IMAP extension abstraction layer (direct IMAP extension usage)  
15. Fallback for servers without MANAGESIEVE (only support servers with capability)