v2.0.2
Ten findings (6 P1, 4 P2) plus a performance issue and two static-analysis notes from a second, independent external review, fixed and verified with new regression tests (197 β 215). One P1 (a UIDVALIDITY-unsafe email ID scheme) is deliberately deferred β see "Known limitation" below.
Fixed β Security
send_test_emailbypassed destructive confirmation andPROTONMAIL_RESTRICT_OUTBOUND_TO_SELFβ unlike every other outbound-send path, it accepted any recipient and free-text body with no confirmation and no self-address enforcement.- A confirmed-alive process's file lock could still be stolen after 30 seconds.
isStale()checked PID liveness first, but on a confirmed-alive result fell through to the plain age check anyway β a legitimately slow holder (long critical section, or resuming from sleep) could have its lock stolen out from under it, reintroducing the exact lost-update race the lock exists to prevent. guardAttachmentOutputPath's containment check still hardcoded/while its own ENOENT fallback branch two lines above it already correctly used the platform path separator β a valid Windows path inside the allowed directory could be rejected as escaping it.move_email/bulk_movebypassed the per-action allowlist, checking only read-only mode.
Fixed β Data integrity
- Switching Proton accounts with the same
PROTONMAIL_DATA_DIRexposed the previous account's data. Nothing checked whether the on-disk SQLite index, delivery queue, snooze, draft, or template store actually belonged to the currently-configured account β reproduced live: a second instance read a private phrase from a different account's index, and handed a different account's still-pending queued send to the wrong SMTP transport. Now writes and verifies a small account-identity marker before any store is opened, refusing with a clear error on mismatch (existing pre-fix data adopts the current account as authoritative going forward β this protects future opens, not a pre-existing collision). send_draftcould deliver the same draft twice when called concurrently β no atomic claim existed between readingdraft.statusand the finalmarkSentwrite. A scheduled send that already fired also left the draft's own status stuck at"draft"forever, so a later manualsend_draftcall passed every guard and delivered a genuine second copy. Both paths now share one atomic claim (draft β sending β sent).bulk_delete/bulk_update_flags/bulk_update_labels's batch-size limit was validated against a different set than what actually executed β a match-based bulk operation resolved its criteria twice (once for the size check, once to execute), and the mailbox could change between the two IMAP round trips. Now resolves to a concrete UID set exactly once and executes against that same set.- Default (no explicit
outputPath) attachment saves silently overwrote same-named files β two attachments sharing a filename, in one message or across separate saves, clobbered each other while the tool still reported both as successfully saved. Now uses atomic exclusive file creation with a numeric-suffix fallback on collision. - Threads beyond the first 5,000 indexed messages silently disappeared from
getThreads/getThreadById, since both built their view from a capped 5,000-message snapshot β a query that plainsearch()still found correctly returned empty, and a previously-validthreadIdcould throw "Thread not found" once the index grew past the cap. Thread lookup and filtered search now query SQLite directly, unbounded by the cap. getSyncCheckpointMap/getStatusdeserialized up to 5,000 message rows just to read sync checkpoints or folder metadata β measured at 5,000 needless calls per checkpoint read. Both now query only what they need.
Known limitation (tracked, deliberately not fixed this release β needs dedicated design work)
- The email ID scheme (
folder::uid::checksum) has no protection against a UIDVALIDITY change. After a mailbox generation change (full recreation, some migration scenarios), an old, checksum-valid ID for a UID can silently resolve to a completely different message now occupying that UID.assertMailboxUidValidityalready exists and works correctly when given an expected value, but nothing currently supplies one. A fix requires extending the ID format (with a documented backward-compatible parse path for existing IDs) and threading the expected value through every single-message and bulk mutation β a real design task, not a surgical patch, and deliberately not forced through under time pressure this release.