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:
@@ -1,9 +0,0 @@
|
||||
export {
|
||||
getEmailAccounts,
|
||||
getEmailAccount,
|
||||
createEmailAccount,
|
||||
deleteEmailAccount,
|
||||
updateEmailAccountStatus,
|
||||
updateEmailAccountSyncMeta,
|
||||
getAllSyncedAccounts,
|
||||
} from './queries';
|
||||
@@ -1,41 +0,0 @@
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { db } from '../db';
|
||||
import { emailAccounts } from './schema';
|
||||
import type { EmailAccountInsert, EmailAccountSelect } from '../types';
|
||||
|
||||
export async function getEmailAccounts(userId: number): Promise<EmailAccountSelect[]> {
|
||||
return db.select().from(emailAccounts).where(eq(emailAccounts.userId, userId));
|
||||
}
|
||||
|
||||
export async function getEmailAccount(id: number): Promise<EmailAccountSelect | undefined> {
|
||||
const [row] = await db.select().from(emailAccounts).where(eq(emailAccounts.id, id));
|
||||
return row;
|
||||
}
|
||||
|
||||
export async function createEmailAccount(params: EmailAccountInsert): Promise<EmailAccountSelect> {
|
||||
const [row] = await db.insert(emailAccounts).values(params).returning();
|
||||
return row!;
|
||||
}
|
||||
|
||||
export async function deleteEmailAccount(id: number, userId: number): Promise<boolean> {
|
||||
const [row] = await db
|
||||
.delete(emailAccounts)
|
||||
.where(and(eq(emailAccounts.id, id), eq(emailAccounts.userId, userId)))
|
||||
.returning({ id: emailAccounts.id });
|
||||
return !!row;
|
||||
}
|
||||
|
||||
export async function updateEmailAccountStatus(id: number, status: string): Promise<void> {
|
||||
await db.update(emailAccounts).set({ status }).where(eq(emailAccounts.id, id));
|
||||
}
|
||||
|
||||
export async function updateEmailAccountSyncMeta(id: number, syncMeta: Record<string, unknown>): Promise<void> {
|
||||
await db.update(emailAccounts).set({ syncMeta }).where(eq(emailAccounts.id, id));
|
||||
}
|
||||
|
||||
export async function getAllSyncedAccounts(): Promise<EmailAccountSelect[]> {
|
||||
return db
|
||||
.select()
|
||||
.from(emailAccounts)
|
||||
.where(and(eq(emailAccounts.status, 'synced'), eq(emailAccounts.enabled, true)));
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { pgTable, serial, integer, text, boolean, timestamp, jsonb, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||
import { users } from '../auth/schema';
|
||||
|
||||
export const emailAccounts = pgTable(
|
||||
'email_accounts',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
userId: integer('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
provider: text('provider').notNull(),
|
||||
email: text('email').notNull(),
|
||||
displayName: text('display_name'),
|
||||
imapHost: text('imap_host').notNull(),
|
||||
imapPort: integer('imap_port').notNull(),
|
||||
imapSecure: boolean('imap_secure').notNull().default(true),
|
||||
authType: text('auth_type').notNull(),
|
||||
credentials: jsonb('credentials').notNull().default({}),
|
||||
enabled: boolean('enabled').notNull().default(true),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
status: text('status').notNull().default('connected'),
|
||||
/**
|
||||
* LEGACY, read-only. The sync position now lives in the account's own `emails.db`
|
||||
* (`sidecar/email/sync-meta.ts`), so the file is self-describing and an account can be moved to
|
||||
* another machine by copying it.
|
||||
*
|
||||
* Kept because it is the one-time backfill source for every account created before that move; the
|
||||
* first sync after it migrates these keys into SQLite and nothing writes here again. Dropping it
|
||||
* would strand any account that has not synced since.
|
||||
*/
|
||||
syncMeta: jsonb('sync_meta').notNull().default({}),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [uniqueIndex('uq_email_accounts_user_email').on(table.userId, table.email)],
|
||||
);
|
||||
@@ -44,7 +44,6 @@ export * from './user-data';
|
||||
// ── Plugins — exported only so tsgo stays clean; nothing mounts them ──────────────────────────────
|
||||
|
||||
export * from './dav';
|
||||
export * from './email';
|
||||
export * from './invoiceshelf';
|
||||
export * from './jellyfin';
|
||||
export * from './notify';
|
||||
|
||||
@@ -44,7 +44,6 @@ export * from './service-connections/schema'; // service_connections
|
||||
|
||||
// ── Plugins — uncomment when the plugin is installed ─────────────────────────────────────────────
|
||||
|
||||
// export * from './email/schema'; // email_accounts officer-email
|
||||
// export * from './notify/schema'; // push_devices officer-notify
|
||||
// export * from './dav/schema'; // dav_app_passwords officer-caldav
|
||||
// export * from './photos/schema'; // photos_config officer-photos
|
||||
|
||||
@@ -14,7 +14,6 @@ import type * as Schema from './schema';
|
||||
//
|
||||
// Same move the query modules made when the split landed — import the feature's schema
|
||||
// directly — which is what lets a table leave the aggregator without breaking anything.
|
||||
import type * as EmailSchema from './email/schema';
|
||||
import type * as NotifySchema from './notify/schema';
|
||||
|
||||
// ── Auth ──
|
||||
@@ -63,9 +62,6 @@ export type ScreenInsert = typeof Schema.screens.$inferInsert;
|
||||
|
||||
// ── Email ──
|
||||
|
||||
export type EmailAccountSelect = typeof EmailSchema.emailAccounts.$inferSelect;
|
||||
export type EmailAccountInsert = typeof EmailSchema.emailAccounts.$inferInsert;
|
||||
|
||||
// ── Server ──
|
||||
|
||||
export type ServerConfigSelect = typeof Schema.serverConfig.$inferSelect;
|
||||
|
||||
Reference in New Issue
Block a user