# AGENTS.md - br-email
## 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
| 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 |
## 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
src/pools.rs:28 — receiver.lock().unwrap() (mutex poisoning = unrecoverable)
src/analyze.rs:66,243,417 — env::current_dir().unwrap() (debug-only paths)
src/lib.rs:356-383 — toml::to_string().unwrap() (serializing known-good struct)
```
### 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)
| 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