email leaves the platform for plugins/email
the sidecar, the router, the table, the screens and the account manager are
gone from src/ entirely - deleted, not commented, because a commented mount is
what assertPermissionTotality cannot see.
three reverse imports had to go first, and they are what decided the boundary.
core may not import a plugin's queries, and one of these had already proved
why: getEmailAccounts was awaited at the top level of the agent sidecar, so
when the table went away on 2026-08-13 the rejection took officer-agent into a
pm2 restart loop with every live chat session inside it (fbb6d6c7).
both survivors now read the DISK instead. findEmailAccountName() in
data-path.ts picks the newest emails.db under an account's email_accounts/
directory - no import, no query, and nothing to throw when the plugin is not
installed. the layout was already the platform's, so this costs no coupling.
the agent keeps its email_db tool and an email-context chat still opens in the
right mailbox.
the google oauth callback no longer appends /email to the dock: a plugin's
route is the plugin's business.
app-store/catalogue.ts entry deleted, and that is the whole point rather than
tidying. permissionAvailability derives from sidecar_installs and a plugin only
ever gets a plugin_installs row, so a leftover entry makes the permission
permanently unavailable - denied route, no dock tile - on a machine where the
plugin is installed and healthy. it has bitten headscale and nearly music.
two app-store tests used email as their config-only fixture; repointed at
notify, which is the same shape.
google-auth stays in core and the plugin imports it via @@/. that direction is
allowed and google-auth knows nothing about mail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,11 +15,10 @@ import { sendClaudeCodeStreaming } from '@@/channels/send-claude-code';
|
||||
import { sendOpenCodeStreaming } from '@@/channels/send-opencode';
|
||||
import * as sidecar from '@@/sidecar-registry';
|
||||
import { join } from 'path';
|
||||
import { getOwnerHomeDir, getEmailAccountsDir } from '../../../servers/data-path';
|
||||
import { getOwnerHomeDir, getEmailAccountsDir, findEmailAccountName } from '../../../servers/data-path';
|
||||
import { attachedFilePaths } from './attached-files';
|
||||
import {
|
||||
getUserSettings,
|
||||
getEmailAccounts,
|
||||
getChatEventsSince,
|
||||
appendChatEvent,
|
||||
getUserById,
|
||||
@@ -124,16 +123,11 @@ export const resolveBaseCwd = (home: string, cwd?: string) => resolveCwd(home, c
|
||||
// DATA_PATH/<owner>/email_accounts/<accountEmail>
|
||||
// `accountEmail` will come from the account selector (msg.contextId) later; for now default to the
|
||||
// owner's first enabled account. Falls back to the email_accounts root if there are no accounts.
|
||||
async function resolveEmailCwd(userId: number, ownerEmail: string, accountEmail?: string): Promise<string> {
|
||||
let account = accountEmail?.trim();
|
||||
if (!account) {
|
||||
try {
|
||||
const accounts = await getEmailAccounts(userId);
|
||||
account = (accounts.find((a) => a.enabled) ?? accounts[0])?.email;
|
||||
} catch (err) {
|
||||
logger.error('Failed to resolve email account for chat cwd', { userId, error: String(err) });
|
||||
}
|
||||
}
|
||||
async function resolveEmailCwd(ownerEmail: string, accountEmail?: string): Promise<string> {
|
||||
// Found on disk, never queried. Email is a plugin and the platform may not import its queries; the
|
||||
// directory layout is already the platform's, so reading it answers the question with no coupling and
|
||||
// nothing to throw when the plugin is not installed.
|
||||
const account = accountEmail?.trim() || findEmailAccountName(ownerEmail);
|
||||
const dir = account ? join(getEmailAccountsDir(ownerEmail), account) : getEmailAccountsDir(ownerEmail);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
@@ -147,7 +141,7 @@ async function resolveChatCwd(
|
||||
userId: number,
|
||||
home: string,
|
||||
): Promise<string> {
|
||||
if (msg.context === 'email') return resolveEmailCwd(userId, email, msg.contextId);
|
||||
if (msg.context === 'email') return resolveEmailCwd(email, msg.contextId);
|
||||
if (msg.context === 'chat') {
|
||||
if (msg.cwd?.trim()) return resolveCwd(home, msg.cwd);
|
||||
// The caller's own home, for everyone.
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { createSidecarProxy } from '../../sidecar/create-proxy';
|
||||
|
||||
// /api/email/* — auth, then forward to officer-email. No routes of its own and no mail knowledge: not a
|
||||
// message, not a folder, not an IMAP or SMTP credential.
|
||||
//
|
||||
// The mail store, the 14 HTTP routes, account CRUD, resync and IMAP validation all used to live in this
|
||||
// directory — ~1,800 lines against a 314-line sidecar, with the sidecar importing BACK into the platform
|
||||
// to reach them. They are the sidecar's now.
|
||||
//
|
||||
// A from-scratch mailbox sync holds a proxied request open well past the 60s idle default, hence the
|
||||
// timeout; it applies to the whole prefix because the proxy must not know which routes are slow.
|
||||
|
||||
const proxy = createSidecarProxy({
|
||||
name: 'email',
|
||||
prefix: '/api/email',
|
||||
timeoutSeconds: 1800,
|
||||
});
|
||||
|
||||
export const emailRouter = proxy.router;
|
||||
|
||||
/** Base URL of the sidecar's HTTP server, or null if it hasn't reported in yet. */
|
||||
export const getEmailServerUrl = proxy.getHttpUrl;
|
||||
@@ -303,18 +303,5 @@ export const googleCallbackHandler = async (ctx: any) => {
|
||||
},
|
||||
});
|
||||
|
||||
// Auto-add /email to dock
|
||||
try {
|
||||
const existing = await getDockPaths(dbUser.id);
|
||||
const paths = existing ?? ['/', '/files', '/automation', '/dashboards', '/chat'];
|
||||
|
||||
if (!paths.includes('/email')) {
|
||||
paths.push('/email');
|
||||
await setDockPaths(dbUser.id, paths);
|
||||
}
|
||||
} catch {
|
||||
// Non-critical — don't block the OAuth flow
|
||||
}
|
||||
|
||||
return ctx.redirect('/settings/integrations?google=success');
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user