Connection form: INBOX is accepted although the form and the README say the inbox is never read #12

Closed
opened 2026-10-01 10:47:57 +00:00 by tiagoagueda · 1 comment
Owner

Found on 2026-09-29 in a read-only review of postulo-imap at cc09c6381, reading validate() against the texts the form and the README show, and running it on the snapshot.

The form's help text, its validation message and the README all promise that Postulo never reads the inbox. Nothing enforces that: validate() only checks that the folder is not blank, so INBOX is saved like any other name. Because a pass flags or moves every message it fetched, recognised or not, the cost of typing it is larger than "the inbox gets read".

  • Where:
    • src/postulo_imap/sync.py:123-129, ImapSync.validate(): the only folder check is if not str(config.get("folder", "")).strip(), and its message is "Name the folder to read. Postulo never reads the inbox." The only move_to check is that it is not blank when mark is move.
    • src/postulo_imap/sync.py:76-80: the help text "One folder, never the inbox. … Postulo reads nothing else."
    • src/postulo_imap/sync.py:214-217: box.mark(uids) and box.move(uids, …) receive every uid the search returned, including the messages for which classify() returned None at sync.py:183-186.
    • README.md:31-33: "Postulo never reads your inbox, and it reads nothing but this one folder." README.md:62-64 adds that for mail the rules do not recognise "your mailbox is left as it was", which is not what sync.py:214-217 does; tests/test_imap.py:561 asserts that the unrecognised message is flagged too.
  • What happens: validate({…, "folder": "INBOX"}) and validate({…, "folder": "inbox", "mark": "move"}) both return {} (run against the snapshot). A person who has not made the Jobs folder yet types INBOX to try the plugin. With the default Flag it mode, every message of the last thirty days in the inbox is fetched whole (BODY.PEEK[]), run through the rules and given the $Postulo keyword. With Move it to a subfolder, all of those messages, recruiter mail or not, leave the inbox for Jobs/Read on the first pass. validate() equally returns {} for mark = move with move_to equal to folder; in that mode the keyword is not used, so every pass fetches and moves the same messages again.
  • Why it matters: the one rule the plugin states three times is not a rule. postulo/postulo#34, which specified the plugin, said whole-mailbox reading would be "a deliberate second step with its own warning"; there is no second step and no warning, and the person finds out when their inbox is empty. Nothing is deleted, so it is recoverable, but it is the opposite of what they were told.
  • Fix: in validate(), refuse a folder that is INBOX after stripping, compared case-insensitively (IMAP treats that one name case-insensitively), with a message that says why; and refuse move_to equal to folder when mark is move. If reading the inbox is to stay possible, put it behind an explicit confirmation field with its own warning, as postulo/postulo#34 described, and soften the three texts to match. Either way, make README.md:62-64 true: say that an unrecognised message is still flagged or moved, or, in Move mode, move only the messages a suggestion was filed for.

Done when

  • tests/test_imap.py has a test in which ImapSync().validate({**CONFIG, "folder": "inbox"}) names folder, and validate({**CONFIG, "mark": "move", "move_to": "Jobs"}) names move_to; both fail today.
  • The help text, the validation message and README.md say what the code does, including what happens to a message the rules do not recognise.
Found on 2026-09-29 in a read-only review of postulo-imap at cc09c6381, reading `validate()` against the texts the form and the README show, and running it on the snapshot. The form's help text, its validation message and the README all promise that Postulo never reads the inbox. Nothing enforces that: `validate()` only checks that the folder is not blank, so `INBOX` is saved like any other name. Because a pass flags or moves every message it fetched, recognised or not, the cost of typing it is larger than "the inbox gets read". - **Where:** - `src/postulo_imap/sync.py:123-129`, `ImapSync.validate()`: the only folder check is `if not str(config.get("folder", "")).strip()`, and its message is "Name the folder to read. Postulo never reads the inbox." The only `move_to` check is that it is not blank when `mark` is `move`. - `src/postulo_imap/sync.py:76-80`: the help text "One folder, never the inbox. … Postulo reads nothing else." - `src/postulo_imap/sync.py:214-217`: `box.mark(uids)` and `box.move(uids, …)` receive every uid the search returned, including the messages for which `classify()` returned `None` at `sync.py:183-186`. - `README.md:31-33`: "Postulo never reads your inbox, and it reads nothing but this one folder." `README.md:62-64` adds that for mail the rules do not recognise "your mailbox is left as it was", which is not what `sync.py:214-217` does; `tests/test_imap.py:561` asserts that the unrecognised message is flagged too. - **What happens:** `validate({…, "folder": "INBOX"})` and `validate({…, "folder": "inbox", "mark": "move"})` both return `{}` (run against the snapshot). A person who has not made the *Jobs* folder yet types `INBOX` to try the plugin. With the default *Flag it* mode, every message of the last thirty days in the inbox is fetched whole (`BODY.PEEK[]`), run through the rules and given the `$Postulo` keyword. With *Move it to a subfolder*, all of those messages, recruiter mail or not, leave the inbox for `Jobs/Read` on the first pass. `validate()` equally returns `{}` for `mark` = `move` with `move_to` equal to `folder`; in that mode the keyword is not used, so every pass fetches and moves the same messages again. - **Why it matters:** the one rule the plugin states three times is not a rule. postulo/postulo#34, which specified the plugin, said whole-mailbox reading would be "a deliberate second step with its own warning"; there is no second step and no warning, and the person finds out when their inbox is empty. Nothing is deleted, so it is recoverable, but it is the opposite of what they were told. - **Fix:** in `validate()`, refuse a folder that is `INBOX` after stripping, compared case-insensitively (IMAP treats that one name case-insensitively), with a message that says why; and refuse `move_to` equal to `folder` when `mark` is `move`. If reading the inbox is to stay possible, put it behind an explicit confirmation field with its own warning, as postulo/postulo#34 described, and soften the three texts to match. Either way, make `README.md:62-64` true: say that an unrecognised message is still flagged or moved, or, in *Move* mode, move only the messages a suggestion was filed for. ## Done when - `tests/test_imap.py` has a test in which `ImapSync().validate({**CONFIG, "folder": "inbox"})` names `folder`, and `validate({**CONFIG, "mark": "move", "move_to": "Jobs"})` names `move_to`; both fail today. - The help text, the validation message and `README.md` say what the code does, including what happens to a message the rules do not recognise.
tiagoagueda added this to the 0.6.0 milestone 2026-10-01 10:47:57 +00:00
Author
Owner

Fixed by pull request #25.

Fixed by pull request #25.
Sign in to join this conversation.
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
Postulo/postulo-imap#12
No description provided.