br-email 1.2.20

This is an Email
Documentation
# AGENTS.md - br-email

**Generated:** 2026-02-23 | **Branch:** main

## OVERVIEW

Rust email library: IMAP/POP3/SMTP with TLS, incremental sync (UID/UIDL), connection reuse with auto-reconnect, retry with exponential backoff, MIME parsing.

## STRUCTURE

```
src/
  lib.rs            # Entry: Mail, Receive/Sender traits, Config, EmailType
  analyze.rs        # MIME parsing: headers, body, attachments (AnalyzeEmails)
  pools.rs          # Thread pool (Pool, Worker, Message)
  imap/
    mod.rs          # Imap struct: Receive/Sender impl, connection reuse, Drop
    server.rs       # Server enum (Stream/StreamTls), UID ops, retry
  pop3/
    mod.rs          # Pop3 struct: Receive/Sender impl, connection reuse, Drop
    server.rs       # Server enum (Stream/StreamTls), UIDL, retry
  smtp/
    mod.rs          # Smtp struct: Sender impl via lettre
tests/
  imap.rs           # Live IMAP tests (needs real server + JSON config)
  pop3.rs           # Live POP3 tests
  smtp.rs           # Live SMTP tests
  incremental_sync.rs  # UID/UIDL sync tests
  index.rs          # fn main() script, NOT #[test]
  analyze.rs        # fn main() script, NOT #[test]
```

## WHERE TO LOOK

| Task | Location | Notes |
|------|----------|-------|
| Add protocol | `src/{proto}/mod.rs` | Impl `Receive` + `Sender` traits from `lib.rs` |
| Email parsing | `src/analyze.rs` | `AnalyzeEmails::new()``header()``body()``parts()` |
| Connection logic | `src/*/server.rs` | `Server::new()``try_connect()``new_with_retry()` |
| Connection reuse | `src/*/mod.rs` | `get_conn()` lazy connect, `reconnect()`, `Drop` logout |
| Incremental sync | `src/imap/server.rs` | `get_uidvalidity()`, `search_since_uid()`, `get_max_uid()` |
| POP3 UIDL | `src/pop3/server.rs` | `uidl()``Vec<(seq, uidl)>` |
| Config loading | `src/lib.rs` | `Config::create(path)` → TOML with `[connections.name]` |
| Attachment saving | `src/analyze.rs` | `set_files()` → temp file in cwd, tracked in `self.files` JSON |
| Date parsing | `src/analyze.rs` | `datetime()` — normalizes month case (`NOV``Nov`) before `chrono` parse |

## COMMANDS

```bash
cargo build                    # Build all features
cargo test --lib               # Unit tests (213 tests, ~6s)
cargo test --test imap         # Live IMAP tests (needs JSON config)
cargo tarpaulin --lib          # Coverage (~98.5%)
cargo fmt && cargo clippy --all-features
```

## CONVENTIONS

### Error Handling
- `std::io::Error` everywhere, created via `Error::other(msg)`
- Chinese error messages: `"IMAP 连接超时: {host} {e}"`, `"读取失败: {e}"`
- Unsupported ops return `Error::other("不支持...")` instead of panicking

### Imports (order)
```rust
use std::io::{Error, Read, Write};  // 1. std
use native_tls::TlsConnector;        // 2. external
use crate::{EmailType, Receive};     // 3. crate
```

### Feature Flags
```toml
default = ["imap", "pop3", "smtp"]
smtp = ["lettre"]  # Only smtp pulls in lettre
```

### Connection Reuse Pattern (IMAP + POP3)
```rust
struct Imap {
    conn: Option<Server>,  // Cached connection
    // ...
}
fn get_conn(&mut self) -> Result<&mut Server, Error>  // Lazy connect
fn reconnect(&mut self) -> Result<&mut Server, Error>  // Drop + reconnect
impl Drop { fn drop(&mut self) { /* logout */ } }
// Each Receive method: try → if error → reconnect → retry once
```

### TLS Port Convention
- `ssl = (port == 993)` for IMAP, `ssl = (port == 995)` for POP3
- Non-standard TLS ports NOT supported (hardcoded in `try_connect`)

### Read Protection
- `read()`: 64KB max, EOF detection
- `read_multi()`: 10MB max, EOF detection
- Both return `Error` on limit exceeded or connection closed

## ANTI-PATTERNS (THIS PROJECT)

### Unsupported Trait Methods Return Error
```rust
// IMAP/POP3 Sender methods → Error::other("不支持...")
imap.set_from("...")   // Returns Err, not panic
pop3.set_to("...")     // Returns Err, not panic
// SMTP Receive methods → Error::other("不支持...")
smtp.get_total()       // Returns Err, not panic
```

### Production `unwrap()` — Remaining Locations
```
src/imap/mod.rs:45    — self.conn.as_mut().unwrap() (after Option check in get_conn)
src/pop3/mod.rs:42    — same pattern
```
Previously `pools.rs` (mutex lock), `analyze.rs` (env::current_dir), `lib.rs` (Config::create) had unwraps — all replaced with safe error handling in 2026-02 audit.

### Live Tests in `tests/`
- `tests/*.rs` require real mail servers + credentials
- Will fail without config files (e.g., `tests/imap_google.json`)
- `tests/index.rs`, `tests/analyze.rs` are `fn main()` scripts, NOT `#[test]`
- Unit tests (`cargo test --lib`) are fully self-contained

### TLS Port Binding
- Tests cannot cover `try_connect` TLS success path (lines 63-68 in IMAP, 67 in POP3)
- Requires binding to port 993/995 (privileged)
- TLS branch coverage uses `Server::StreamTls` direct construction instead

## INCREMENTAL SYNC API

### IMAP (UID-based)
```rust
let validity = recv.get_validity()?;      // UIDVALIDITY — if changed, full resync
let max_uid = recv.get_max_uid()?;         // Highest UID in mailbox
let new_uids = recv.get_new_since("100")?; // UIDs > 100
```

### POP3 (UIDL-based)
```rust
let uidl_map = recv.get_uidl_map()?;  // Vec<(seq_str, uidl_str)>
```

## CONNECTION RETRY

Both IMAP/POP3 in `server.rs`:
```rust
const MAX_RETRIES: u32 = 3;
const CONNECT_TIMEOUT_SECS: u64 = 30;
const READ_TIMEOUT_SECS: u64 = 60;
const WRITE_TIMEOUT_SECS: u64 = 30;
// Backoff: 2s, 4s, 8s (base * 2^attempt, capped at 16s)
```

## TESTING

```bash
cargo test --lib                        # All 213 unit tests
cargo test analyze::tests::             # analyze.rs tests (51)
cargo test imap::server::tests::        # IMAP server tests (57)
cargo test pop3::server::tests::        # POP3 server tests (36)
cargo test imap::tests::                # IMAP mod tests (14)
cargo test pop3::tests::                # POP3 mod tests (12)
cargo test smtp::tests::               # SMTP tests (14)
cargo test pools::tests::              # Pool tests (6)
cargo test tests::                      # lib.rs tests (23)
```

### Test Architecture
- **Mock TCP servers**: `start_mock_imap_server()`, `start_mock_pop3_server()` — bind random port, replay scripted responses
- **Mock TLS servers**: `start_mock_tls_imap_server()`, `start_mock_tls_pop3_server()` — self-signed cert via `openssl` CLI, `OnceLock` cached `TlsAcceptor`
- **Custom readers**: `ChunkedReader` (partial reads), `ErrorReader` (always fails), `EofReader` (returns 0), `InfiniteReader` (never terminates)
- **Helper parsers**: Test-only response parsers mirror production logic for assertion clarity

### Coverage: 98.5% (1151/1169 lines)
| Module | Coverage |
|--------|----------|
| analyze.rs | 100% |
| lib.rs | 100% |
| imap/mod.rs | 100% |
| pop3/mod.rs | 100% |
| imap/server.rs | 96.5% |
| pop3/server.rs | 96.3% |
| smtp/mod.rs | 94.8% |
| pools.rs | 97.4% |

Uncoverable: TLS `try_connect` success (privileged ports), SMTP `send()` success (needs real server), `pools` worker channel error (private types).

## NOTES

- `coverage/` dir contains generated tarpaulin HTML — gitignore candidate
- `ureq` + `br-crypto` deps used for OAuth/crypto but OAuth module not in current source
- Attachment temp files written to `env::current_dir()` — caller must clean up
- `EmailType` enum (`Google`, `QQ`, `None`) affects IMAP FETCH command format