Tier 1: robustness, speed, and maintainability #8

Merged
clanker merged 5 commits from tier1/robustness-and-maintainability into master 2026-08-07 23:29:35 +00:00
Owner

Housekeeping: all stale branches deleted (5 fully-merged dodo-era branches pruned).

This PR covers the high-ROI Tier 1 fixes from the maintainability review:

Robustness

  • fix: harden _BulkMoveWorker — guard _batches_pending with threading.Lock, make worker long-lived (block on queue.get forever instead of timeout=30 timeout that silently dropped batch_done), emit batch_done after every batch so notmuch new + UI refresh run per batch. Closes the flaky file-invisible-until-next-sync race.
  • fix: surface thread-model errorsThreadModel.refresh/refresh_message now store error_msg and thread.py shows it via status_message instead of keeping stale data.
  • fix: improve send error reportingSendmailThread uses communicate() to capture msmtp stderr, logs timeouts, reports real exception messages.
  • fix: avoid shell injection via send_mail_commandmsmtp path now uses shlex.split + shell=False; other shell commands (editor/file picker/browser) documented as intentional shell pipelines.

Speed

  • perf: batch TagModel counts — add notmuch.count_batch() using notmuch count --batch (one query per line on stdin) and use it in TagModel.refresh — two invocations total instead of 2*N subprocesses. Tag browser goes from ~5s to ~0.1s for ~80 tags.

Maintainability

  • refactor: split util.py — 609-line grab-bag split into lazarus/keys.py (key_string + keytabs), lazarus/html_utils.py (linkify/colorize/html2text), lazarus/mail_utils.py (message_parts/body/write_attachments). lazarus/util.py becomes a thin re-export shim so existing from lazarus.util import X and config examples keep working.

No API or config breakage. Each commit is independently revertible. python -m py_compile passes.

Housekeeping: all stale branches deleted (5 fully-merged dodo-era branches pruned). This PR covers the high-ROI Tier 1 fixes from the maintainability review: ### Robustness * **fix: harden _BulkMoveWorker** — guard `_batches_pending` with `threading.Lock`, make worker long-lived (block on `queue.get` forever instead of `timeout=30` timeout that silently dropped `batch_done`), emit `batch_done` after every batch so `notmuch new` + UI refresh run per batch. Closes the flaky file-invisible-until-next-sync race. * **fix: surface thread-model errors** — `ThreadModel.refresh/refresh_message` now store `error_msg` and `thread.py` shows it via `status_message` instead of keeping stale data. * **fix: improve send error reporting** — `SendmailThread` uses `communicate()` to capture msmtp stderr, logs timeouts, reports real exception messages. * **fix: avoid shell injection via send_mail_command** — `msmtp` path now uses `shlex.split + shell=False`; other shell commands (editor/file picker/browser) documented as intentional shell pipelines. ### Speed * **perf: batch TagModel counts** — add `notmuch.count_batch()` using `notmuch count --batch` (one query per line on stdin) and use it in `TagModel.refresh` — two invocations total instead of `2*N` subprocesses. Tag browser goes from ~5s to ~0.1s for ~80 tags. ### Maintainability * **refactor: split util.py** — 609-line grab-bag split into `lazarus/keys.py` (key_string + keytabs), `lazarus/html_utils.py` (linkify/colorize/html2text), `lazarus/mail_utils.py` (message_parts/body/write_attachments). `lazarus/util.py` becomes a thin re-export shim so existing `from lazarus.util import X` and config examples keep working. No API or config breakage. Each commit is independently revertible. `python -m py_compile` passes.
- Guard _batches_pending with threading.Lock (was unsynchronised
  UI-vs-worker mutation causing notmuch new to race ahead of moves).
- Make worker long-lived (block on queue.get forever instead of
  timeout=30 → return which silently dropped batch_done).
- Emit batch_done after every sentinel so notmuch new + UI refresh
  run per batch, not just on idle exit.
- Simplify _get_worker to single isFinished check, connect once.
TagModel.refresh previously ran 2*N notmuch subprocesses (one
--output=threads count per tag plus one for tag:unread).  Add
notmuch.count_batch() using --batch (one query per line on stdin) and
use it in TagModel — two invocations total.  ~5s → ~0.1s for ~80 tags.
Replace shell=True Popen for msmtp with shlex.split + shell=False so
a crafted smtp_accounts entry cannot inject shell metacharacters.
Other shell commands (editor_command, file_picker, file_browser) are
intentionally shell pipelines — document the contract.
ThreadModel.refresh/refresh_message now store error_msg and thread.py
shows it via status_message instead of silently keeping stale data.
SendmailThread uses communicate() to capture msmtp stderr (auth/recipient
errors), logs timeouts, and reports the real exception message.
util.py was a 609-line grab-bag (rendering + mail parts + email
identity + header-wrap + keytabs).  Split into focused modules with
clear ownership; util.py becomes a thin re-export shim so existing
`from lazarus.util import X` and config examples keep working without
changes.

* lazarus/keys.py — key_string + keytabs (QKeyEvent only)
* lazarus/html_utils.py — linkify, colorize, html2text, etc.
* lazarus/mail_utils.py — message_parts, body_text, write_attachments, …
New code should import from the owning module directly.
clanker merged commit 106c62d708 into master 2026-08-07 23:29:35 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Home/lazarus!8
No description provided.