plan of record for extracting email into a plugin
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
# 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: mapped, decisions taken, not yet moved.**
|
||||
|
||||
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 — TAKEN
|
||||
|
||||
`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.
|
||||
Reference in New Issue
Block a user