- Python 100%
|
Some checks failed
CI / test (push) Failing after 45s
`tests/test_surface.py` kept a `REACHING_PAST` table: three modules this plugin imported past `postulo.plugins.api`, each with the reason and each pointing at postulo/postulo#229. The table was never a list of exemptions -- it was the map of what the core still had to move. The core moved them. `Application` and `Suggestion`, `suggest`, and `Contact` are on the surface, so `sync.py` and `matching.py` import them from there and the table goes empty. A plugin that reads somebody's mailbox and files guesses against their applications now names nothing of Postulo's that is not promised. The AST check goes with it. It was this repository's own copy of the one Postulo runs over the plugins it ships, and the core publishes that now as `postulo.plugins.testing` -- so the file imports it instead. One check rather than a copy that drifts, and it resolves relative imports, which the copy did not. What is left here is what is particular to this plugin: that its six names are on `__all__` rather than merely reachable, because `__all__` is what the promise is made about. The core is pinned at `1d6cdd7e0`, which is where those names start. Closes #2 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .forgejo/workflows | ||
| src/postulo_imap | ||
| tests | ||
| .gitignore | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
postulo-imap
Read one folder of your mailbox and let Postulo suggest what it says happened: acknowledgements, rejections, interview invitations, assessments, offers. Nothing is written into your record on a guess.
Every application produces mail, and every piece of it has to be typed into the timeline by hand, which is why timelines go stale. This plugin reads the folder you chose, works out which application each message is about, decides what kind of message it is from phrase lists anyone can read, and files a suggestion. You accept or decline each one; neither answer is asked for twice.
IMAP, not POP3, deliberately. POP3 has no folders, no flags and no server-side state: it downloads, and usually deletes. IMAP lets Postulo read one folder, mark what it has read, and leave everything else alone — the only acceptable way to put a job tracker near a mailbox.
Install
Into the environment of a running Postulo:
uv pip install git+https://source.tiagoagueda.com/postulo/postulo-imap.git
Restart Postulo. In a container, bake it into the image — see Plugins in the image on Installing Postulo.
Use
First make the folder. In your mail client or on your server, create a folder — the default name here is Jobs — and a rule that files recruiter mail into it. Postulo never reads your inbox, and it reads nothing but this one folder.
Then, under Settings → Connections → Add a connection, choose Mailbox (IMAP):
| Field | What it is |
|---|---|
| Server, port, security | imap.example.org, 993 with TLS, or 143 with STARTTLS. |
| Username, password | An app password wherever the server offers one. Gmail requires one with two-factor authentication on. Stored encrypted. |
| Folder to read | Jobs, or whatever you called it. |
| Once read | Flag with $Postulo (default, invisible in most clients), move to a subfolder, or leave the mailbox completely untouched. |
| Look back | How many days a pass considers. Thirty by default. |
| Run | Every 15 minutes to once a day, honoured by the scheduler; Sync now runs at once. |
Press Test: it signs in and says how many messages are waiting.
What it does with a message
Matching, strongest evidence first: a reply to a message already matched; the sender's address against a contact you recorded; the sender's domain against the company's website (public mail providers prove nothing and are ignored); and last, the company name or the role title in the subject or body. When two applications answer equally well — two roles at one company — nothing is chosen, and you attach it by hand.
Classifying, from the phrase lists in src/postulo_imap/rules/: German, English,
Spanish, French, Italian, Dutch, Polish and Portuguese. Rules, not a model: they are
inspectable, translatable and wrong in ways you can see. The most consequential reading
wins, so a message that both thanks you and invites you is an invitation. An invitation's
dates are offered as the message wrote them, for you to put in the diary.
A language with no phrase list is skipped, not guessed at. Mail in one of the eight is read; anything else matches nothing, no suggestion is filed, and your mailbox is left as it was. Postulo's own interface speaks many more languages than this plugin can read mail in, so if your rejections arrive in a language that is not listed, the list is what to add.
Suggesting. Every recognised message becomes a suggestion in Applications → Suggestions: what it says, which application, who it is from, which phrase was recognised, and — for a rejection, an offer, an invitation — the status it would move the application to. Accepting writes it through Postulo's own services, so the timeline says imap put it there and you can undo it by hand. Declining is remembered.
What is stored: the message's identifier, date, sender, subject and a short excerpt. Not the body, beyond that excerpt, and no attachments.
Adding a language
Copy src/postulo_imap/rules/en.json, translate the phrases, name the file after the
language, add it to the set named in tests/test_imap.py, and open a pull request. No code
changes, and the tests will show you whether your patterns catch what they should.
Keep to words that belong to your language. The files are tried in turn and the first match wins, so a pattern that reads equally well as English or Dutch will answer for those too, and the suggestion will name the wrong language.
Develop
uv sync # brings in Postulo itself from its repository, for the tests
uv run pytest
uv run ruff check .
The tests run against a mailbox that lives in memory — folders, keyword search, fetch, flags, move — and inside a real Postulo through its registry and scheduler. No connection leaves the machine.
Licence
AGPL-3.0-or-later, like Postulo.