IMAP and Gmail sync into a per-account SQLite store, with the mail read and composed in the browser. moved whole from src/servers/sidecar/email (14 files), src/servers/api/email, officer_db/src/email, and the Screens/Dashboard/Email tree. what changed on the way out: - credentials are encrypted. the column was plaintext jsonb holding an IMAP app password that the SMTP transport read straight out of the row; it is now AES-256-GCM under this plugin's own 'email' secret-store purpose, sealed and opened in queries.ts alone so nothing above it can hold ciphertext by accident. no plaintext fallback on read - a value that does not verify is an error. - the router derives its prefix from mountPrefix() instead of the literal '/api/email', which was only correct for a first-party publisher. - the frontend became panels.ts + layout.ts. the two panel appTypes that were null are now declared, because the platform screen used to override them by panel id and a plugin has no such hook. - account management became a modal. it was a section in the platform's Settings, which a plugin cannot contribute to, and a panel does not work either: PluginScreen renders the workspace locked, so a panel outside the default layout is unreachable. it follows Compose, which had already solved this. - 17 palette classes replaced with semantic tokens. not verified: this has never run. it was switched off in the platform on 2026-08-13 and its table was never created, so nothing has exercised sync, sending or the UI since. the first install is the first test.
73 lines
2.6 KiB
TypeScript
73 lines
2.6 KiB
TypeScript
import type { SidecarEvent } from '@@/sidecar/protocol';
|
|
import { initEmailCron, stopEmailCron } from './email-cron';
|
|
import { initEmailIdle, stopEmailIdle } from './email-idle';
|
|
import { broadcastEmailNew } from './routes';
|
|
import { startEmailServer } from './http';
|
|
import { createSidecarConnector } from '@@/sidecar/connect';
|
|
import { API_URL } from '@@/officer-url.mjs';
|
|
|
|
// The sidecar used to reach BACK into the platform's queue over this socket to get a sync run —
|
|
// enqueueViaWs / listJobsViaWs and a pending-response map. Syncs run in this process now
|
|
// (sync-runner.ts), so the shim is gone and nothing but a port crosses the socket at startup.
|
|
|
|
// ── Command handlers ──
|
|
|
|
type ReplyFn = (msg: SidecarEvent) => void;
|
|
|
|
function handleCommand(cmd: Record<string, unknown>, reply: ReplyFn) {
|
|
switch (cmd.type) {
|
|
case 'ping':
|
|
reply({ type: 'pong', id: cmd.id as string });
|
|
break;
|
|
|
|
default:
|
|
reply({
|
|
type: 'error',
|
|
id: cmd.id as string,
|
|
error: `Unknown command type: ${cmd.type}`,
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── HTTP server ──
|
|
//
|
|
// Started before the registration socket so the port is known by the time we announce it. `/api/email/*`
|
|
// on the platform is a proxy onto this.
|
|
const serverPort = startEmailServer();
|
|
|
|
// ── Connect to API server ──
|
|
|
|
const connection = createSidecarConnector({
|
|
apiUrl: `${API_URL}/api/sidecar/register`,
|
|
name: 'email',
|
|
handles: ['email'],
|
|
onCommand(cmd, reply) {
|
|
handleCommand(cmd as Record<string, unknown>, reply as ReplyFn);
|
|
},
|
|
onConnected() {
|
|
// The platform forgets the port when the socket drops, and this listener outlives an officer restart,
|
|
// so re-announce on every reconnect.
|
|
connection.send({ type: 'email:server', port: serverPort });
|
|
// Start email cron once connected (so queue commands can reach API server)
|
|
initEmailCron();
|
|
// Real-time push via IMAP IDLE; the cron above is the slow backstop. On new mail, tell the API
|
|
// server so it can push an SSE event to that user's open /email page.
|
|
// Straight to this process's own SSE clients — the /email/events stream lives here now, so the
|
|
// round trip out to officer and back (the `email:new` wire event) is gone.
|
|
initEmailIdle((userEmail) => broadcastEmailNew(userEmail));
|
|
},
|
|
});
|
|
|
|
// ── Graceful shutdown ──
|
|
|
|
function shutdown(signal: string) {
|
|
console.log(`[email] ${signal} received, shutting down...`);
|
|
stopEmailIdle();
|
|
stopEmailCron();
|
|
connection.destroy();
|
|
process.exit(0);
|
|
}
|
|
|
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
process.on('SIGINT', () => shutdown('SIGINT'));
|