mailshift 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. mailshift-1.0.0/.claudeignore +70 -0
  2. mailshift-1.0.0/.gitignore +35 -0
  3. mailshift-1.0.0/AGENTS.md +71 -0
  4. mailshift-1.0.0/CLAUDE.md +70 -0
  5. mailshift-1.0.0/LICENSE +674 -0
  6. mailshift-1.0.0/PKG-INFO +258 -0
  7. mailshift-1.0.0/README.md +246 -0
  8. mailshift-1.0.0/benchmark.py +34 -0
  9. mailshift-1.0.0/benchmark_lower.py +24 -0
  10. mailshift-1.0.0/blacklist.json +299 -0
  11. mailshift-1.0.0/first.png +0 -0
  12. mailshift-1.0.0/main.py +12 -0
  13. mailshift-1.0.0/ollama-vs-lmstudio.md +69 -0
  14. mailshift-1.0.0/pyproject.toml +22 -0
  15. mailshift-1.0.0/second.png +0 -0
  16. mailshift-1.0.0/src/mailshift/__init__.py +1 -0
  17. mailshift-1.0.0/src/mailshift/config/__init__.py +1 -0
  18. mailshift-1.0.0/src/mailshift/config/config.py +279 -0
  19. mailshift-1.0.0/src/mailshift/core/__init__.py +1 -0
  20. mailshift-1.0.0/src/mailshift/core/analyzers/__init__.py +1 -0
  21. mailshift-1.0.0/src/mailshift/core/analyzers/base.py +8 -0
  22. mailshift-1.0.0/src/mailshift/core/analyzers/fast.py +123 -0
  23. mailshift-1.0.0/src/mailshift/core/analyzers/pro.py +529 -0
  24. mailshift-1.0.0/src/mailshift/core/engine.py +842 -0
  25. mailshift-1.0.0/src/mailshift/core/session.py +268 -0
  26. mailshift-1.0.0/src/mailshift/db/__init__.py +1 -0
  27. mailshift-1.0.0/src/mailshift/db/database.py +199 -0
  28. mailshift-1.0.0/src/mailshift/main.py +902 -0
  29. mailshift-1.0.0/src/mailshift/models/__init__.py +1 -0
  30. mailshift-1.0.0/src/mailshift/models/models.py +47 -0
  31. mailshift-1.0.0/src/mailshift/ui/__init__.py +1 -0
  32. mailshift-1.0.0/src/mailshift/ui/cli.py +919 -0
  33. mailshift-1.0.0/src/mailshift/ui/styles.py +96 -0
  34. mailshift-1.0.0/src/mailshift/utils/__init__.py +1 -0
  35. mailshift-1.0.0/src/mailshift/utils/hardware.py +572 -0
  36. mailshift-1.0.0/src/mailshift/utils/history.py +358 -0
  37. mailshift-1.0.0/src/mailshift/utils/logger.py +78 -0
  38. mailshift-1.0.0/src/mailshift/utils/paths.py +8 -0
  39. mailshift-1.0.0/src/mailshift/utils/power_user_settings.py +85 -0
  40. mailshift-1.0.0/src/mailshift/utils/unsubscribe.py +119 -0
  41. mailshift-1.0.0/src/mailshift/utils/updater.py +136 -0
  42. mailshift-1.0.0/src/mailshift/utils/worker_profile_store.py +284 -0
  43. mailshift-1.0.0/tests/__init__.py +0 -0
  44. mailshift-1.0.0/tests/benchmark_matrix_helpers.py +519 -0
  45. mailshift-1.0.0/tests/benchmark_regex.py +62 -0
  46. mailshift-1.0.0/tests/evaluate_ai.py +120 -0
  47. mailshift-1.0.0/tests/evaluate_fast.py +108 -0
  48. mailshift-1.0.0/tests/evaluate_lm_studio_workers.py +186 -0
  49. mailshift-1.0.0/tests/evaluate_pro_mode.py +345 -0
  50. mailshift-1.0.0/tests/evaluate_worker_matrix.py +239 -0
  51. mailshift-1.0.0/tests/standalone_benchmark.py +95 -0
  52. mailshift-1.0.0/tests/test_ai_dataset.json +4002 -0
  53. mailshift-1.0.0/tests/test_analyzer_base.py +8 -0
  54. mailshift-1.0.0/tests/test_benchmark_matrix_helpers.py +111 -0
  55. mailshift-1.0.0/tests/test_cli_installers_cross_platform.py +49 -0
  56. mailshift-1.0.0/tests/test_cli_security.py +35 -0
  57. mailshift-1.0.0/tests/test_config.py +207 -0
  58. mailshift-1.0.0/tests/test_database.py +124 -0
  59. mailshift-1.0.0/tests/test_decode_header.py +47 -0
  60. mailshift-1.0.0/tests/test_engine.py +469 -0
  61. mailshift-1.0.0/tests/test_engine_filters.py +54 -0
  62. mailshift-1.0.0/tests/test_engine_reconnect.py +80 -0
  63. mailshift-1.0.0/tests/test_fast_analyzer.py +157 -0
  64. mailshift-1.0.0/tests/test_fast_analyzer_dataset.py +40 -0
  65. mailshift-1.0.0/tests/test_google_delete_trash.py +112 -0
  66. mailshift-1.0.0/tests/test_hardware.py +457 -0
  67. mailshift-1.0.0/tests/test_history.py +138 -0
  68. mailshift-1.0.0/tests/test_keyword_and_normalize.py +54 -0
  69. mailshift-1.0.0/tests/test_logger.py +79 -0
  70. mailshift-1.0.0/tests/test_main.py +572 -0
  71. mailshift-1.0.0/tests/test_main_features.py +60 -0
  72. mailshift-1.0.0/tests/test_models.py +41 -0
  73. mailshift-1.0.0/tests/test_parsing_integrated.py +34 -0
  74. mailshift-1.0.0/tests/test_parsing_isolated.py +124 -0
  75. mailshift-1.0.0/tests/test_paths.py +54 -0
  76. mailshift-1.0.0/tests/test_power_user_settings.py +28 -0
  77. mailshift-1.0.0/tests/test_pro_analyzer.py +300 -0
  78. mailshift-1.0.0/tests/test_session.py +164 -0
  79. mailshift-1.0.0/tests/test_worker_profile_store.py +155 -0
  80. mailshift-1.0.0/tests/verify_fix.py +40 -0
  81. mailshift-1.0.0/tests/verify_fix_standalone.py +55 -0
  82. mailshift-1.0.0/tests/verify_windows_fix.py +68 -0
  83. mailshift-1.0.0/whitelist.json +261 -0
@@ -0,0 +1,70 @@
1
+ # Claude ignore: exclude common build, cache, env, logs, and sensitive files
2
+
3
+ # Node
4
+ node_modules/
5
+ npm-debug.log*
6
+ yarn-debug.log*
7
+ pnpm-debug.log*
8
+
9
+ # Python virtual environments and caches
10
+ .venv/
11
+ venv/
12
+ env/
13
+ ENV/
14
+ __pycache__/
15
+ *.py[cod]
16
+ *$py.class
17
+ .pytest_cache/
18
+ .mypy_cache/
19
+
20
+ # Build and packaging
21
+ build/
22
+ dist/
23
+ *.egg-info/
24
+ .eggs/
25
+ pip-wheel-metadata/
26
+ target/
27
+ src-tauri/target/
28
+
29
+ # Logs, databases and runtime files
30
+ logs/
31
+ logs/**
32
+ *.log
33
+ mailshift.db
34
+ *.sqlite
35
+ *.sqlite3
36
+ *.db
37
+
38
+ # Coverage and test artifacts
39
+ .coverage
40
+ coverage.xml
41
+ htmlcov/
42
+
43
+ # Editor/IDE directories
44
+ .vscode/
45
+ .idea/
46
+ *.sublime-workspace
47
+ *.sublime-project
48
+
49
+ # OS and temp files
50
+ .DS_Store
51
+ Thumbs.db
52
+ tmp/
53
+ temp/
54
+ *.tmp
55
+ *.bak
56
+ *.swp
57
+
58
+ # Notebooks
59
+ .ipynb_checkpoints/
60
+
61
+ # Secrets and local env
62
+ .env
63
+ .env.*
64
+ secrets/
65
+ .secrets/
66
+ local_settings.py
67
+
68
+ # Misc caches and tools
69
+ .cache/
70
+ .tox/
@@ -0,0 +1,35 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ .Python
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ .eggs/
11
+ *.db
12
+ *.log
13
+ *.txt
14
+ credentials.json
15
+ worker_profiles.json
16
+ power_user_settings.json
17
+
18
+ # Virtual environments
19
+ .venv/
20
+ venv/
21
+ env/
22
+
23
+ # Pytest
24
+ .pytest_cache/
25
+ .coverage
26
+ htmlcov/
27
+
28
+ # Editors
29
+ .idea/
30
+ .vscode/
31
+ *.swp
32
+ *.swo
33
+
34
+ # Logs
35
+ logs/
@@ -0,0 +1,71 @@
1
+ # AGENTS.md
2
+
3
+ ## Must-follow constraints
4
+
5
+ - Decision strings are **Turkish**: `"SIL"` (delete) and `"TUT"` (keep). Never use English equivalents anywhere in `ScanResult.decision`.
6
+ - `dry_run=True` is the default. Actual deletion only happens with `--no-dry-run`. Never change this default.
7
+ - On any LLM or analysis error, fall back to `"TUT"` — never `"SIL"`. Safety-first.
8
+ - Emails with `has_attachment=True` must always return `"TUT"` (enforced in `fast_analyzer.py`). Do not remove this guard.
9
+ - Only update `AGENTS.md` and `README.md` if the changes introduce new conventions, logic, or significant architectural shifts. Minor bugfixes or trivial cleanups do not require documentation updates.
10
+ - `IMAPConfig.password` Pydantic kütüphanesi ile `SecretStr` olarak tanımlanmıştır. Değerine erişmek için her zaman `.get_secret_value()` kullanılmalıdır, doğrudan öznitelik erişimi (`.password`) yasaktır. Bu sayede parola loglarda veya hata çıktılarında açık şekilde görünmez.
11
+
12
+ ## Validation before finishing
13
+
14
+ ```
15
+ py -3.14 -m pytest tests/
16
+ ```
17
+
18
+ No other build or lint step is configured.
19
+
20
+ - Gmail delete/move-to-trash behavior is covered by `tests/test_google_delete_trash.py`.
21
+
22
+ ## Repo-specific conventions
23
+
24
+ - **Python interpreter**: `py -3.14` — not `python`, not `python3`.
25
+ - **Keyword lists**: `whitelist.json` and `blacklist.json` are plain JSON arrays at the project root. `JUNK_PATTERN` and `WHITELIST_PATTERN` are compiled once at import time in `config.py`. Runtime mutations via `add_to_blacklist()` / `add_to_whitelist()` update the JSON file but do **not** update the in-memory regex — a process restart is required for changes to take effect in analysis.
26
+ - **Fast analyzer safety guards**: `fast_analyzer.py` has a built-in keep-guard that forces `TUT` for premium lifecycle expiry notices, verification-code/OTP mails, and Drive/cloud storage fullness alerts before junk matching. Keep this guard intact unless there is a stronger safety replacement.
27
+ - **Fast analyzer order**: Decision flow is `has_attachment -> whitelist(TUT) -> safety-guard(TUT) -> blacklist(SIL) -> no match(TUT)`. Preserve this order unless a requirement explicitly changes it.
28
+ - **Fast analyzer text split**: Whitelist and safety-guard checks run against `full_text` (subject + sender + body_preview). Blacklist matching runs against `content_text` (subject + body_preview only, **excluding sender**) to prevent false positives from legitimate automated senders such as `no-reply@github.com` matching the `"no-reply"` rule.
29
+ - **Turkish case normalization**: `fast_analyzer._normalize()` replaces `İ` (U+0130) with `i` then lowercases before matching. Both `full_text` and `content_text` pass through this function, so all regex matches return lowercase tokens in `ScanResult.reason`.
30
+ - **`analyzer.py`** is a re-export shim only. Do not add logic there.
31
+ - **Console output**: Use `from ui import console` (Rich console) for any new terminal output. Do not use bare `print()` — stdout is wrapped for UTF-8 on Windows and `print()` bypasses Rich's rendering.
32
+ - **Progress UI stability**: Keep per-item progress labels short/sanitized (single-line, no control chars) and prefer ASCII status tags (`SIL`/`TUT`) over emoji to avoid wrapped/duplicated-looking progress bars in narrow Windows terminals.
33
+ - **Logger stream**: Keep the console logger on `sys.stderr` so Rich progress output on stdout is not visually disrupted by warning/error logs.
34
+ - **No YAML**: The project uses JSON throughout. `requirements.txt` does not include pyyaml.
35
+
36
+ ## Important locations
37
+
38
+ - `mailshift.db` — SQLite cache, created at **CWD** (not a fixed path). Contains `mails_cache` (header/body cache) and `fetch_checkpoint` (resume table). Persists across runs. Delete it or call `clear_mails_cache()` to force a fresh scan.
39
+ - `whitelist.json` / `blacklist.json` — project root, not in a subdirectory.
40
+ - **OS Keyring** (Windows Credential Manager) — Kimlik bilgileri `keyring` kütüphanesi ile OS düzeyinde şifreli olarak saklanır. Proje dizininde düz metin / JSON şifre dosyası tutulmaz.
41
+
42
+ ## Change safety rules
43
+
44
+ - Pro mode is **two-phase**: heuristic runs first; LLM is called only for `"SIL"` candidates. Any change to fast analysis directly affects what the LLM sees.
45
+ - Proton Mail requires Bridge running locally at `127.0.0.1:1143` (no SSL). Gmail is `imap.gmail.com:993` (SSL). These are set via `PROVIDER_DEFAULTS` in `config.py`.
46
+ - `delete_mails()` and `move_to_trash()` both call `self._conn.expunge()` after flagging. Removing expunge silently leaves messages flagged but not deleted.
47
+ - Delete/trash chunk operations and expunge are retry-protected; on socket/SSL EOF-style errors MailEngine reconnects and re-selects `INBOX` before the next retry.
48
+ - `fetch_headers_concurrent` is sequential at the IMAP level (single connection, chunked). The name is misleading — do not add actual per-thread IMAP connections without careful locking.
49
+ - `AppConfig`, `IMAPConfig`, and `OllamaConfig` are frozen Pydantic models. Do not attempt in-place mutation.
50
+ - `hardware.py` detects NVIDIA first, then Intel/AMD GPUs on Windows via `Win32_VideoController` (PowerShell CIM query). Shared VRAM can be estimated from system RAM when `AdapterRAM` is not usable.
51
+
52
+ ## Known gotchas
53
+
54
+ - `JUNK_PATTERN` / `WHITELIST_PATTERN` are `None` if the respective JSON file is empty — always guard with `if JUNK_PATTERN:` before calling `.search()`.
55
+ - `ScanStats.errors` uses a mutable default in a dataclass — it is initialised via `__post_init__`. Do not set it as a class-level default list.
56
+ - `database.py` resolves `DB_FILE = Path("mailshift.db")` relative to CWD at import time. If tests change directory, the DB lands in the wrong place.
57
+ - `list_uids()` returns UIDs in **reverse order** (newest first) due to `[::-1]` before applying `scan_limit`. Ordering assumptions matter when reasoning about which emails are skipped by `scan_limit`.
58
+ - `pro_analyzer.py` decision parsing accepts plain-text and JSON-like LLM outputs, and normalizes Turkish dotted/dotless `i` so `SİL/SIL/sıl` are treated consistently. Keep the safety fallback to `"TUT"` when no valid decision is found.
59
+ - `pro_analyzer.py` uses Ollama `/api/chat` with a JSON decision schema (`decision: SIL|TUT`) and reads `message.content` first (then `response` fallback). This avoids empty-output failures seen with some small models on `/api/generate`.
60
+ - `pro_analyzer.py` sends `think: false` and a higher `num_predict` budget for `qwen3.5:2B/4B` so the model does not spend output tokens on hidden reasoning and leave `message.content` empty.
61
+ - **LM Studio Offloading**: Pro mode attempts to unload models from LM Studio VRAM immediately after analysis completion via `POST /api/v1/models/unload` to free up system resources.
62
+ - **Ollama Visibility**: Ollama on Windows often runs without a visible window or system tray icon. To completely terminate it, users must use the Task Manager.
63
+ - **LLM Provider Selection**: Pro mode now supports two backends — Ollama (default, recommended NVIDIA GPU users, "Easy Setup") and LM Studio (recommended all users). Selected via `prompt_llm_backend()` in `cli_utils.py`. `prompt_mode()` returns `(Mode, model_name, llm_backend)`.
64
+ - **Ollama launch helpers**: `_launch_ollama_process()`, `stop_ollama()`, and `_is_ollama_running()` in `cli_utils.py` are reusable primitives used by `start_ollama()`. `stop_ollama()` uses `taskkill /F /IM ollama.exe` on Windows and `pkill -f ollama` on other platforms.
65
+ - **Post-install UX**: When Ollama is newly installed from interactive Pro-mode flow, MailShift prints a "next steps" panel (restart terminal, `ollama serve`, rerun Pro mode). Keep this guidance visible on fallback-to-fast paths and do not require manual `ollama pull` there.
66
+ - **Model selection UX**: Recommended model list includes `qwen3.5:0.8B` with `%95 Accurate` label, and missing recommended models are auto-downloaded/verified from the selection screen.
67
+ - **Worker policy source-of-truth**: Resolve worker decisions via `resolve_worker_plan()` in `hardware.py` and pass the resolved value through runtime; `calculate_optimal_workers()` is kept as a compatibility wrapper.
68
+ - **Manual worker safety clamp**: Manual worker values must be capped to the computed safe upper limit (backend/VRAM/RAM/CPU) and CLI should show a clear warning when clamping occurs.
69
+ - **Fast mode worker UX**: Fast mode does not use worker-based parallel analysis; keep configuration output explicit as not-used to avoid user confusion.
70
+ - **Worker profile learning cache**: Auto worker runs in Pro mode can persist learned recommendations in `worker_profiles.json` using hashed device signature + backend/model keys; warm-start applies only to auto plans and still obeys safe upper limits/manual override rules.
71
+ - **Power-user worker probe**: `--power-worker-probe` enables a persisted hardware probe preference (`power_user_settings.json`) and can seed initial worker recommendation as `auto-probe` when no profile warm-start exists.
@@ -0,0 +1,70 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ # Install dependencies
9
+ pip install -r requirements.txt
10
+
11
+ # Run all tests
12
+ py -3.14 -m pytest tests/
13
+
14
+ # Run a single test file
15
+ py -3.14 -m pytest tests/test_fast_analyzer.py
16
+
17
+ # Run the app
18
+ python main.py
19
+ ```
20
+
21
+ **Python interpreter**: Always use `py -3.14`, not `python` or `python3`. No lint step is configured.
22
+
23
+ ## Architecture
24
+
25
+ MailShift is a privacy-first, AI-assisted Gmail/Proton/IMAP mail cleaner. Entry point: `main.py` (root) → `src/mailshift/main.py` (Click CLI).
26
+
27
+ **Data flow for a scan**:
28
+ 1. `main.py` + `ui/cli.py` — collect provider, credentials (stored in OS Keyring), scan mode, LLM backend
29
+ 2. `config/config.py` — assembles frozen Pydantic models (`AppConfig`, `IMAPConfig`, `OllamaConfig`/`LMStudioConfig`); `PROVIDER_DEFAULTS` maps provider names to IMAP hosts
30
+ 3. `core/engine.py` (`MailEngine`) — IMAP connect, `list_uids()` (returns newest-first), `fetch_headers_concurrent()` (chunked, single connection despite the name), SQLite checkpoint resume via `db/database.py`
31
+ 4. **Fast mode** (`core/analyzers/fast.py`): heuristic-only. Decision order: `has_attachment → whitelist(TUT) → safety-guard(TUT) → blacklist(SIL) → no-match(TUT)`
32
+ 5. **Pro mode** (`core/analyzers/pro.py`): two-phase — Fast runs first, then LLM (Ollama or LM Studio) is called only for `SIL` candidates
33
+ 6. `core/session.py` — `LLMWorker` (threaded) + Rich progress handlers
34
+ 7. `core/engine.py` — `delete_mails()` / `move_to_trash()`: chunk, flag `\Deleted`, expunge, retry on SSL/EOF
35
+ 8. `utils/history.py` — JSON logs under `logs/`, CSV/JSON export; `ui/styles.py` — Rich result tables
36
+ 9. **Unsubscribe flow** (`utils/unsubscribe.py`): after scan results are shown, `_prompt_unsubscribe()` in `main.py` collects all `SIL` results that carry a `List-Unsubscribe` HTTP URL, deduplicates by URL, and presents a 4-option menu (auto-all / select / export / skip). Triggered in both dry-run and live paths.
37
+
38
+ **Key files**:
39
+ - `blacklist.json` / `whitelist.json` — project root, compiled to regex at import time in `config.py`. `JUNK_PATTERN`/`WHITELIST_PATTERN` may be `None` if empty — always guard with `if pattern:` before `.search()`
40
+ - `mailshift.db` — SQLite cache at CWD (not a fixed path); delete to force fresh scan
41
+ - `utils/unsubscribe.py` — `UnsubscribeEntry`, `build_unsubscribe_entries()`, `perform_unsubscribe()`, `export_unsubscribe_links()`
42
+
43
+ ## Critical Conventions
44
+
45
+ - **Decision strings are Turkish**: `"SIL"` (delete), `"TUT"` (keep). Never use English equivalents in `ScanResult.decision`.
46
+ - **`dry_run=True` is the immutable default.** Actual deletion only occurs with `--no-dry-run`.
47
+ - **Any LLM or analysis error must fall back to `"TUT"`**, never `"SIL"`. Safety-first.
48
+ - **Emails with `has_attachment=True` always return `"TUT"`** — do not remove this guard in `fast_analyzer.py`.
49
+ - **`IMAPConfig.password` is `SecretStr`** — access via `.get_secret_value()` only, never direct attribute.
50
+ - **Fast analyzer text split**: whitelist + safety-guard check `full_text` (subject + sender + body); blacklist checks `content_text` (subject + body only, sender excluded) to prevent false positives from senders like `no-reply@github.com`.
51
+ - **Turkish normalization**: `fast_analyzer._normalize()` replaces `İ` (U+0130) → `i` before lowercasing. All regex tokens in `ScanResult.reason` are lowercase.
52
+ - **Console output**: Use `from ui import console` (Rich). Never use bare `print()` — it bypasses Rich's UTF-8 wrapper.
53
+ - **Logger on `sys.stderr`**: keeps Rich progress on stdout visually clean.
54
+ - **No YAML**: JSON throughout (`blacklist.json`, `whitelist.json`, logs).
55
+ - `analyzer.py` is a re-export shim only — do not add logic there.
56
+ - Pydantic models `AppConfig`, `IMAPConfig`, `OllamaConfig` are frozen — no in-place mutation.
57
+
58
+ ## Gotchas
59
+
60
+ - `MailMeta.unsubscribe_url` is populated from the `List-Unsubscribe` header during `_fetch_mails_bulk()` using `_UNSUB_URL_RE` (HTTP-only; `mailto:` entries are silently ignored). The second body-fetch pass only updates `body_preview`, so the URL from the first header fetch is preserved.
61
+ - `database.py` auto-migrates old DBs: `init_db()` runs `ALTER TABLE mails_cache ADD COLUMN unsubscribe_url TEXT DEFAULT ''` if the column is absent.
62
+ - `perform_unsubscribe()` tries GET first, then falls back to an RFC 8058 one-click POST (`List-Unsubscribe=One-Click`). Never import `requests` for this — use stdlib `urllib.request` only.
63
+ - `fetch_headers_concurrent` is sequential at the IMAP level (single connection). Do not add per-thread IMAP connections without careful locking.
64
+ - Runtime mutations via `add_to_blacklist()` / `add_to_whitelist()` update JSON but **do not update the in-memory regex** — a process restart is needed.
65
+ - `list_uids()` reverses UIDs (`[::-1]`) so newest-first; `scan_limit` skips oldest emails.
66
+ - `database.py` resolves `DB_FILE` relative to CWD at import time — if tests change directory, the DB lands in the wrong place.
67
+ - `ScanStats.errors` is initialized in `__post_init__`, not as a class-level default list.
68
+ - Pro mode sends `think: false` and higher `num_predict` for `qwen3.5` models to avoid empty `message.content` from hidden reasoning tokens.
69
+ - LM Studio: after analysis, Pro mode unloads the model from VRAM via `POST /api/v1/models/unload`.
70
+ - Proton Mail requires Bridge at `127.0.0.1:1143` (no SSL); Gmail is `imap.gmail.com:993` (SSL).