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 { 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 Error('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, ): 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 Error('Failed to update email account'); 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 }; }