← back to Norma

app/api/gmail/send/route.ts

120 lines

import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
import { getOrgId } from '@/lib/orgId';
import { getGmailClient } from '@/lib/gmail';

/**
 * POST /api/gmail/send
 *
 * Send or draft an email via Gmail API, then save to email_campaigns.
 *
 * Body: {
 *   messageId?: string,      // reply to this message
 *   body_html?: string,
 *   body_text: string,
 *   action: 'send' | 'draft',
 *   to?: string,
 *   subject?: string,
 * }
 */
export async function POST(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
  const body = await request.json();
  const { messageId, body_html, body_text, action, to, subject } = body;

  if (!body_text) {
    return NextResponse.json({ error: 'body_text is required' }, { status: 400 });
  }

  // Look up original message for reply context
  let originalMsg: { subject: string; from_address: string } | null = null;
  if (messageId) {
    const msgRes = await query<{ subject: string; from_address: string }>(
      `SELECT subject, from_address FROM gmail_messages WHERE id = $1`,
      [messageId],
    );
    originalMsg = msgRes.rows[0] || null;
  }

  const replySubject = subject || (originalMsg ? `Re: ${originalMsg.subject || '(no subject)'}` : 'Email from SDCC');
  const replyTo = to || originalMsg?.from_address || '';
  const status = action === 'send' ? 'sent' : 'draft';

  // Try to send via Gmail API
  let gmailSent = false;
  if (action === 'send' && replyTo) {
    const gmail = await getGmailClient();
    if (gmail) {
      try {
        const htmlBody = body_html || `<p>${body_text.replace(/\n/g, '</p><p>')}</p>`;
        const rawEmail = [
          `To: ${replyTo}`,
          `Subject: ${replySubject}`,
          `Content-Type: text/html; charset=utf-8`,
          '',
          htmlBody,
        ].join('\r\n');

        const encoded = Buffer.from(rawEmail).toString('base64url');

        await gmail.users.messages.send({
          userId: 'me',
          requestBody: { raw: encoded },
        });

        gmailSent = true;
        console.log(`[gmail-send] Sent to ${replyTo}: ${replySubject}`);
      } catch (err) {
        console.error('[gmail-send] Gmail API send failed:', (err as Error).message);
        // Fall through to save as campaign anyway
      }
    }
  }

  // Save to email_campaigns
  const res = await query(
    `INSERT INTO email_campaigns (
       subject, body_html, body_text, target_email, target_name,
       status, email_type, tone, ai_generated, ai_model,
       sent_at, org_id
     ) VALUES (
       $1, $2, $3, $4, $5,
       $6, 'gmail_reply', 'professional', true, 'gemini-2.0-flash',
       ${status === 'sent' ? 'NOW()' : 'NULL'}, $7
     ) RETURNING id, status, created_at`,
    [
      replySubject,
      body_html || `<p>${body_text.replace(/\n/g, '</p><p>')}</p>`,
      body_text,
      replyTo,
      replyTo,
      status,
      orgId,
    ],
  );

  await auditLog('gmail.reply', 'email_campaign', res.rows[0].id, {
    action,
    originalMessageId: messageId,
    to: replyTo,
    gmailSent,
  });

  return NextResponse.json({
    success: true,
    id: res.rows[0].id,
    status: res.rows[0].status,
    gmailSent,
    message: gmailSent
      ? `Email sent via Gmail to ${replyTo}`
      : action === 'send'
      ? 'Saved to campaigns (Gmail not connected — send manually)'
      : 'Draft saved',
  });
}