import { and, eq } from 'drizzle-orm'; import { db } from '@/lib/db'; import { emailAccounts } from '@/lib/db/schema/email'; import { encrypt, decrypt } from '@/lib/utils/encryption'; import { createAuditLog, type AuditMeta } from '@/lib/audit'; import { CodedError, NotFoundError, ForbiddenError } from '@/lib/errors'; import type { ConnectAccountInput, ToggleAccountInput } from '@/lib/validators/email'; // ─── Types ──────────────────────────────────────────────────────────────────── type AccountWithoutCredentials = Omit; // ─── Helpers ────────────────────────────────────────────────────────────────── function stripCredentials(account: typeof emailAccounts.$inferSelect): AccountWithoutCredentials { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { credentialsEnc: _, ...safe } = account; return safe; } // ─── List ───────────────────────────────────────────────────────────────────── export async function listAccounts( userId: string, portId: string, ): Promise { const accounts = await db .select() .from(emailAccounts) .where(and(eq(emailAccounts.userId, userId), eq(emailAccounts.portId, portId))); return accounts.map(stripCredentials); } // ─── Connect ────────────────────────────────────────────────────────────────── export async function connectAccount( userId: string, portId: string, data: ConnectAccountInput, audit: AuditMeta, ): Promise { const credentialsEnc = encrypt( JSON.stringify({ username: data.username, password: data.password }), ); const inserted = await db .insert(emailAccounts) .values({ userId, portId, provider: data.provider, emailAddress: data.emailAddress, smtpHost: data.smtpHost, smtpPort: data.smtpPort, imapHost: data.imapHost, imapPort: data.imapPort, credentialsEnc, isActive: true, }) .returning(); const account = inserted[0]; if (!account) throw new CodedError('INSERT_RETURNING_EMPTY', { internalMessage: 'Failed to insert email account', }); void createAuditLog({ userId: audit.userId, portId: audit.portId, action: 'create', entityType: 'email_account', entityId: account.id, metadata: { emailAddress: data.emailAddress, provider: data.provider }, ipAddress: audit.ipAddress, userAgent: audit.userAgent, }); return stripCredentials(account); } // ─── Toggle ─────────────────────────────────────────────────────────────────── export async function toggleAccount( accountId: string, userId: string, data: ToggleAccountInput, audit?: AuditMeta, ): Promise { const existing = await db.query.emailAccounts.findFirst({ where: eq(emailAccounts.id, accountId), }); if (!existing) { throw new NotFoundError('Email account'); } if (existing.userId !== userId) { throw new ForbiddenError('You do not own this email account'); } const updatedRows = await db .update(emailAccounts) .set({ isActive: data.isActive, updatedAt: new Date() }) .where(eq(emailAccounts.id, accountId)) .returning(); const updated = updatedRows[0]; if (!updated) throw new CodedError('INSERT_RETURNING_EMPTY', { internalMessage: 'Failed to update email account', }); // H-05: enable/disable used to land silently between connect/disconnect. // Audit-trail this so an admin can see the toggle history (silently // disabling an account suppresses bounce detection or reroutes replies - // compliance-relevant change). if (audit) { void createAuditLog({ userId: audit.userId, portId: audit.portId, action: 'update', entityType: 'email_account', entityId: accountId, oldValue: { isActive: existing.isActive }, newValue: { isActive: updated.isActive }, metadata: { emailAddress: existing.emailAddress }, ipAddress: audit.ipAddress, userAgent: audit.userAgent, }); } return stripCredentials(updated); } // ─── Disconnect ─────────────────────────────────────────────────────────────── export async function disconnectAccount( accountId: string, userId: string, audit: AuditMeta, ): Promise { const existing = await db.query.emailAccounts.findFirst({ where: eq(emailAccounts.id, accountId), }); if (!existing) { throw new NotFoundError('Email account'); } if (existing.userId !== userId) { throw new ForbiddenError('You do not own this email account'); } await db.delete(emailAccounts).where(eq(emailAccounts.id, accountId)); void createAuditLog({ userId: audit.userId, portId: audit.portId, action: 'delete', entityType: 'email_account', entityId: accountId, metadata: { emailAddress: existing.emailAddress }, ipAddress: audit.ipAddress, userAgent: audit.userAgent, }); } // ─── Get Decrypted Credentials (INTERNAL ONLY) ──────────────────────────────── export async function getDecryptedCredentials( accountId: string, ): Promise<{ username: string; password: string }> { const account = await db.query.emailAccounts.findFirst({ where: eq(emailAccounts.id, accountId), }); if (!account) { throw new NotFoundError('Email account'); } const { username, password } = JSON.parse(decrypt(account.credentialsEnc)) as { username: string; password: string; }; return { username, password }; }