162 lines
10 KiB
Markdown
162 lines
10 KiB
Markdown
# Extracting email into a plugin — plan of record
|
|
|
|
Started 2026-08-17. The runbook is `plugins/EXTRACTING-A-PLUGIN.md` and it is the authority; this file only
|
|
records what is specific to email — the boundary, the decisions taken, and what they cost.
|
|
|
|
**Status: the plugin side is built and typechecks. The platform has NOT been touched.**
|
|
|
|
Everything so far is additive, inside `plugins/email/` — which is gitignored, so the platform tree is
|
|
unchanged and email is exactly as switched-off as it was. Nothing is broken and nothing needs reverting.
|
|
|
|
The owner asked for a full extraction with no interim step ("no temporary steps"), and was explicit that
|
|
speed is not the constraint. Email is therefore NOT being switched back on in place first, even though that
|
|
is a four-line change — doing so would create `email_accounts` as a core-owned table that the extraction
|
|
would immediately have to hand over.
|
|
|
|
---
|
|
|
|
## What email actually is
|
|
|
|
~5,500 lines, and the shape is better than its reputation.
|
|
|
|
| part | where | lines | difficulty |
|
|
| ----------- | ------------------------------------------------- | ----- | -------------------------------------------------------------------------------------------------------- |
|
|
| sidecar | `src/servers/sidecar/email/` (14 files) | 3,697 | easy — self-contained, already announces `email:server` through the generalised `${string}:server` union |
|
|
| api router | `src/servers/api/email/router.ts` | 22 | trivial — a `createSidecarProxy` with a 1800s timeout that is load-bearing |
|
|
| database | `officer_db/src/email/` | 85 | easy — ONE table, `email_accounts`, already carrying the `email_` prefix a plugin requires |
|
|
| frontend | `officer-web/Screens/Dashboard/Email/` | 1,150 | easy — already a `WorkspaceView` with three panels |
|
|
| accounts UI | `Settings/IntegrationsSettings/EmailAccounts.tsx` | 509 | moves into the plugin |
|
|
| types | `workspaces/types/email.ts` | 29 | moves whole |
|
|
|
|
Two findings that made this smaller than the survey suggested:
|
|
|
|
- **The frontend is already panel-shaped.** `EmailScreen` renders `WorkspaceView` with a `components` map of
|
|
`email-list` / `email-reader` / `email-chat` and a `defaultLayout`. The plugin contract wants exactly that,
|
|
declared as `web/panels.ts` + `web/layout.ts`. This is a re-declaration, not a rewrite.
|
|
- **Theme debt is 17 palette classes**, all in the status/badge colours. The runbook requires paying this
|
|
down during the extraction; at this size that is minutes, not a project.
|
|
|
|
**`src/workspaces/emailer/` is NOT part of this.** It is the outbound transactional mailer — React Email
|
|
templates, `sendMail`, `getTransport` — used by `forgot-password.ts`, `server-settings/smtp.ts` and the
|
|
queue engine, all core. It shares the word "email" and the `nodemailer` dependency and nothing else.
|
|
|
|
---
|
|
|
|
## The boundary, and the one thing that decides it
|
|
|
|
A plugin importing platform code is fine (`@@/…`). The reverse is not, and email has three of them.
|
|
|
|
### `google-auth` is not a problem
|
|
|
|
Five sidecar files import `getValidGoogleAccessToken` / `refreshGoogleAccessToken` from
|
|
`api/integrations/google-auth`, which core `integrations.ts` also uses. The survey called this the hardest
|
|
problem in the extraction; it is not one at all. That direction is plugin→platform, which the runbook
|
|
explicitly permits, and `google-auth` is generic Google OAuth that knows nothing about mail. **It stays in
|
|
core and the plugin imports it via `@@/`.** The only real work is that the import style is inconsistent
|
|
today (`../../` in three files, `@@/` in two), so a mechanical rewrite would miss cases.
|
|
|
|
### The real problem: core reads email's database
|
|
|
|
Three sites, and the first has already taken the platform down once (`fbb6d6c7`, 2026-08-13 — an unguarded
|
|
top-level await on `getEmailAccounts` rejected and put `officer-agent` into a PM2 restart loop, taking every
|
|
live chat session with it).
|
|
|
|
1. `sidecar/claude/user-instance.ts` — queries `email_accounts`, picks the first enabled one, and bakes
|
|
`OFFICER_EMAIL_DB` into the agent's MCP config so the `email_db` tool can read it.
|
|
2. `api/chat/websocket.ts` — `resolveEmailCwd` queries the same table so a chat with `context: 'email'`
|
|
runs in that account's directory.
|
|
3. `api/integrations/integrations.ts` — the Google OAuth callback appends `/email` to the user's dock.
|
|
|
|
**Note the `email_db` tool is not in this repository.** It lives in `PI_TOOLS_DIRS`, resolved at runtime, so
|
|
it is invisible to grep and will not follow the extraction wherever the boundary lands.
|
|
|
|
---
|
|
|
|
## Decisions
|
|
|
|
The owner was asked about the first two and had stepped away; both are taken on the lower-risk option and
|
|
both are reversible. **Revisit them before believing this document is settled.**
|
|
|
|
### 1. Keep `email_db` working, by convention rather than by import — TAKEN, REVERSIBLE
|
|
|
|
Core stops importing `getEmailAccounts` and instead finds the newest `DATA_PATH/<user>/email_accounts/*/emails.db`
|
|
on disk. Same for `resolveEmailCwd`.
|
|
|
|
- **Removes the code dependency** that caused the crash — no core file imports anything email again.
|
|
- **Keeps the feature.** "Ask the agent about my mail" is a large part of why this product is worth running,
|
|
and silently deleting it during a refactor is not a trade the refactor gets to make on its own.
|
|
- **Costs** a vocabulary leak: core still knows the directory name `email_accounts`. The runbook says the
|
|
platform must not know a plugin's vocabulary, so this is a deliberate, recorded exception rather than an
|
|
oversight. The clean version is decision 1c below.
|
|
|
|
Rejected for now: **clean cut** (purest, but removes a working feature by fiat) and **plugin ships its own
|
|
MCP tool** (correct long-term; needs a plugin→agent tool mechanism that does not exist, so it would block
|
|
this extraction on new platform work).
|
|
|
|
### 2. Account management becomes a panel in the email workspace — TAKEN, REVERSIBLE
|
|
|
|
`EmailAccounts.tsx` leaves core Settings and becomes a panel. The plugin contract allows panels and nothing
|
|
else, so this is the shape available; the alternative is a second plugin route. **User-visible change:** you
|
|
will add and remove mail accounts from `/email`, not from `/settings/integrations`.
|
|
|
|
### 3. Encrypt the credentials — REVERSED, deferred to the plugin
|
|
|
|
Originally taken on the reasoning that the table has never held a row here, so encryption was free. That
|
|
reasoning only covers THIS machine — another install may have rows from before 2026-08-13, and a format
|
|
change would strand them.
|
|
|
|
The stronger argument is about verification: a pure move is checkable, because any first-install failure is
|
|
the move. A move plus a data-format change gives a failure two causes, on a feature nothing has exercised in
|
|
four days. Encryption is now the first item in the plugin's own README.
|
|
|
|
### 3b. What the original decision said — TAKEN, then reversed
|
|
|
|
`email_accounts.credentials` is jsonb holding an IMAP app password in the clear. It is encrypted through
|
|
`officerdb/crypto` under its own `email` secret-store purpose as part of the move.
|
|
|
|
This is normally the kind of scope creep to refuse. It is free here for one reason: **the table does not
|
|
exist and has never held a row** — it was commented out of `schema.ts` on 2026-08-13 and `db:push` therefore
|
|
never created it. There is no migration, no backfill and no compatibility window. Doing it later would need
|
|
all three.
|
|
|
|
### 4. `email_accounts` stays in `USER_DIRS` — TAKEN
|
|
|
|
Core pre-creates the directory for every account at 0700. Follows from decision 1: core has to know the path
|
|
anyway, and having two places create it differently is the bug `USER_DIRS` exists to prevent.
|
|
|
|
---
|
|
|
|
## Order
|
|
|
|
Runbook order, with email's specifics:
|
|
|
|
1. `plugins/email/` skeleton + `manifest.ts`. `appName` is the directory name.
|
|
2. **Sidecar** — 14 files, whole. Rewrite the five `google-auth` imports to `@@/`.
|
|
3. **Database** — `officer_db/src/email/` → `plugins/email/db/`. Schema takes `officerdb/auth/schema` for
|
|
the `users.id` FK, the one reference a plugin may make. Encrypt `credentials` (decision 3).
|
|
4. **API** — the 22-line proxy router → `plugins/email/api/router.ts`, exporting `router`.
|
|
5. **Frontend** — Screens/Email + EmailAccounts → `web/panels.ts` + `web/layout.ts`. Pay down the 17 palette
|
|
classes here.
|
|
6. **Core cleanup — delete, never comment.** `hono.ts` mount + import, `registry.ts` entry (both `api` AND
|
|
the still-live `routes: ['/email']`), `App.tsx` routes, `Screens/Dashboard/index.tsx`, `usePageTitle.ts`,
|
|
the Settings section, `types/index.ts`, **both** database barrels, and the three reverse imports above.
|
|
7. **Delete the `app-store/catalogue.ts` entry.** Not optional and not cosmetic: `permissionAvailability`
|
|
derives from `sidecar_installs`, a plugin only ever gets a `plugin_installs` row, so a leftover catalogue
|
|
entry makes the permission permanently `unavailable` and blanks the screen on a machine where the plugin
|
|
is installed and healthy. This has bitten twice already — headscale, and nearly music.
|
|
8. `bunx tsgo` until clean. It finds the wiring that greps miss.
|
|
9. Verify live through the runbook's table: install → API → screen → dock → permissions → disable → enable →
|
|
uninstall → **purge** → install again. Purge is the only honest test of a first install.
|
|
10. Its own public repository, a README, and a marketplace catalogue row — an extraction is not finished
|
|
when it installs from disk.
|
|
|
|
## What is NOT known yet
|
|
|
|
**Whether email works.** It has been off since 2026-08-13 and `email_accounts` has never existed on this
|
|
machine, so nothing has exercised IMAP sync, Gmail sync, sending or the UI in that time. The sidecar starts
|
|
cleanly and its only error is the missing table — which is evidence the code did not rot, and not evidence
|
|
that it works.
|
|
|
|
So the first install is also the first functional test this feature has had in four days, and a failure then
|
|
is as likely to be pre-existing as extraction damage. Diff against git history before assuming which.
|