← 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',
});
}