postulo-thunderbird: bind a mail to a listing from the client, with no mailbox credentials on the server #271

Open
opened 2026-09-17 21:59:18 +00:00 by tiagoagueda · 0 comments
Owner

A Thunderbird MailExtension that sends the message you are reading to Postulo, bound to
the listing or application it is about. One click, one message, from the client.

Its own repository, postulo/postulo-thunderbird, following the shape the browser extensions
already have rather than the Python plugin shape — it never appears in the plugin registry,
because it runs on the person's machine and talks to the API.

Why this is not postulo-imap

They answer the same question and make opposite trades, and both should exist.

postulo-imap postulo-thunderbird
Credentials the mailbox password or an app password, held by the server none, ever
What it sees the folder, polled the one message the person chose
When on a schedule on a click
Fails by going quiet, or suggesting noise not being installed

Giving a job tracker the keys to your mailbox is a real decision, and plenty of people will
not make it. This asks for nothing: the mail never leaves Thunderbird except the message the
person deliberately sends, and the server learns nothing about the rest of the mailbox — not
its size, not its folders, not who else writes to them.

Why it is not a third build of postulo-chromium

postulo-firefox is not a separate extension: source.json pins a postulo-chromium
commit and npm run check:pin refuses anything that moves, so the two build to the same
bytes on purpose. Thunderbird cannot join that arrangement. A MailExtension has
messageDisplay and messages.* where the browser extension has tabs and a page DOM; the
half that reads a job advert out of HTML has no counterpart here.

What it can share, and should, is the Postulo-facing half the browser extensions already
solved: storing a token, configuring the instance URL, calling the API, and asking whether
this is already known before sending. That half is worth lifting out rather than writing a
third time.

It depends on #270, and should not start before it

There is no way to bind a mail to a listing today. #270 is the issue that decides whether
that binding is an event, an attachment, or a separate model, and what happens to it when a
listing becomes an application.

This plugin is a consumer of whatever #270 settles. Starting it first would mean inventing
the shape in an extension repository and then having core disagree with it.

What it needs from the API, and one thing that is missing

Bearer tokens with scopes are already there (api/auth.py, api/models.py). The current
scopes are captures, read, write and documents:read.

None of them fits. Binding a mail is not capturing, and write is far more than a mail
client should hold — that scope covers "applications, listings, notes, reminders, letters".
A token that can attach a message should not also be able to change an application's status.

So #270, or this issue, should add a narrower scope. Worth deciding with #270 rather than
bolting one on afterwards.

Message-ID is the idempotency key, and the machinery exists

api/idempotency.py already lets a capture carry an Idempotency-Key, so that a client
which never saw the 201 can retry without making a second record — and the key belongs to
the account rather than the token, "because a person retrying from a second tool is retrying
the same gesture"
.

A mail's Message-ID is globally unique and stable by design. Using it as the key means the
same message sent twice — from two folders, from two machines, after a failed send — is one
binding, with no new mechanism. It also means the extension does not need to remember what it
has already sent.

Scope

Sends: the headers that identify the message (From, To, Subject, Date,
Message-ID), the body as text, and — chosen, never automatically — attachments.
messages.listAttachments and getAttachmentFile are how. A forwarded job description PDF
is #270's own first example.

Never sends: anything the person did not select. No folder scan, no background sync, no
address book, no credentials.

Shows: which listing or application the message would attach to, found by asking the API,
with the person confirming. Never guessed silently.

What it inherits

  • Foreign text. A mail body is text somebody else wrote, arriving in an application that
    exports to CSV and ICS and serves an MCP client. That is exactly #218, and everything bound
    through this path is inside its blast radius.
  • Its own catalogues. Every plugin carries its own translations and core never translates
    plugin strings; an extension's own interface strings are its own to ship.

To check before starting

  • Which manifest version the current Thunderbird ESR accepts for MailExtensions, and whether
    the target is ESR or release.
  • Distribution: addons.thunderbird.net has its own review process, separate from AMO, and
    postulo-firefox already carries the reproducible-source discipline a review wants.
  • Whether the shared Postulo-facing half is extracted into a small package both extensions
    depend on, or copied once and kept in step by hand. The browser extensions chose pinning;
    this is the moment to decide whether that scales to three.
A Thunderbird MailExtension that sends **the message you are reading** to Postulo, bound to the listing or application it is about. One click, one message, from the client. Its own repository, `postulo/postulo-thunderbird`, following the shape the browser extensions already have rather than the Python plugin shape — it never appears in the plugin registry, because it runs on the person's machine and talks to the API. ## Why this is not `postulo-imap` They answer the same question and make opposite trades, and both should exist. | | `postulo-imap` | `postulo-thunderbird` | | --- | --- | --- | | Credentials | the mailbox password or an app password, held by the server | none, ever | | What it sees | the folder, polled | the one message the person chose | | When | on a schedule | on a click | | Fails by | going quiet, or suggesting noise | not being installed | Giving a job tracker the keys to your mailbox is a real decision, and plenty of people will not make it. This asks for nothing: the mail never leaves Thunderbird except the message the person deliberately sends, and the server learns nothing about the rest of the mailbox — not its size, not its folders, not who else writes to them. ## Why it is not a third build of `postulo-chromium` `postulo-firefox` is not a separate extension: `source.json` pins a `postulo-chromium` commit and `npm run check:pin` refuses anything that moves, so the two build to the same bytes on purpose. Thunderbird cannot join that arrangement. A MailExtension has `messageDisplay` and `messages.*` where the browser extension has tabs and a page DOM; the half that reads a job advert out of HTML has no counterpart here. What it *can* share, and should, is the Postulo-facing half the browser extensions already solved: storing a token, configuring the instance URL, calling the API, and asking whether this is already known before sending. That half is worth lifting out rather than writing a third time. ## It depends on #270, and should not start before it There is no way to bind a mail to a listing today. #270 is the issue that decides whether that binding is an event, an attachment, or a separate model, and what happens to it when a listing becomes an application. This plugin is a *consumer* of whatever #270 settles. Starting it first would mean inventing the shape in an extension repository and then having core disagree with it. ## What it needs from the API, and one thing that is missing Bearer tokens with scopes are already there (`api/auth.py`, `api/models.py`). The current scopes are `captures`, `read`, `write` and `documents:read`. **None of them fits.** Binding a mail is not capturing, and `write` is far more than a mail client should hold — that scope covers "applications, listings, notes, reminders, letters". A token that can attach a message should not also be able to change an application's status. So #270, or this issue, should add a narrower scope. Worth deciding with #270 rather than bolting one on afterwards. ## `Message-ID` is the idempotency key, and the machinery exists `api/idempotency.py` already lets a capture carry an `Idempotency-Key`, so that a client which never saw the `201` can retry without making a second record — and the key belongs to the account rather than the token, *"because a person retrying from a second tool is retrying the same gesture"*. A mail's `Message-ID` is globally unique and stable by design. Using it as the key means the same message sent twice — from two folders, from two machines, after a failed send — is one binding, with no new mechanism. It also means the extension does not need to remember what it has already sent. ## Scope **Sends:** the headers that identify the message (`From`, `To`, `Subject`, `Date`, `Message-ID`), the body as text, and — chosen, never automatically — attachments. `messages.listAttachments` and `getAttachmentFile` are how. A forwarded job description PDF is #270's own first example. **Never sends:** anything the person did not select. No folder scan, no background sync, no address book, no credentials. **Shows:** which listing or application the message would attach to, found by asking the API, with the person confirming. Never guessed silently. ## What it inherits - **Foreign text.** A mail body is text somebody else wrote, arriving in an application that exports to CSV and ICS and serves an MCP client. That is exactly #218, and everything bound through this path is inside its blast radius. - **Its own catalogues.** Every plugin carries its own translations and core never translates plugin strings; an extension's own interface strings are its own to ship. ## To check before starting - Which manifest version the current Thunderbird ESR accepts for MailExtensions, and whether the target is ESR or release. - Distribution: addons.thunderbird.net has its own review process, separate from AMO, and `postulo-firefox` already carries the reproducible-source discipline a review wants. - Whether the shared Postulo-facing half is extracted into a small package both extensions depend on, or copied once and kept in step by hand. The browser extensions chose pinning; this is the moment to decide whether that scales to three.
tiagoagueda added this to the 0.6.0 milestone 2026-09-17 21:59:18 +00:00
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#271
No description provided.