import { type ComponentType, type CSSProperties, createElement, type ReactNode } from 'react' import { Body, Head, Html, Link, Markdown, Section, Text } from '@react-email/components' import { render } from '@react-email/render' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { getBaseUrl } from '@/lib/core/utils/urls' import * as agentmail from '@/lib/mothership/inbox/agentmail-client' import { replaceUntilStable } from '@/lib/mothership/inbox/format' import type { InboxTask } from '@/lib/mothership/inbox/types' const logger = createLogger('InboxResponse') interface InboxResponseContext { inboxProviderId: string | null workspaceId: string } /** * Send the mothership execution result as an email reply via AgentMail. * Returns the AgentMail response message ID for thread stitching, or null on failure. */ export async function sendInboxResponse( inboxTask: InboxTask, result: { success: boolean; content: string; error?: string }, ctx: InboxResponseContext ): Promise { if (!ctx.inboxProviderId || !inboxTask.agentmailMessageId) { logger.warn('Cannot send response: missing inbox provider or message ID', { taskId: inboxTask.id, }) return null } const chatUrl = inboxTask.chatId ? `${getBaseUrl()}/workspace/${ctx.workspaceId}/chat/${inboxTask.chatId}` : `${getBaseUrl()}/workspace/${ctx.workspaceId}/home` const text = result.success ? `${result.content}\n\n[View full conversation](${chatUrl})\n\nBest,\nMothership` : `I wasn't able to complete this task.\n\nError: ${result.error || 'Unknown error'}\n\n[View details](${chatUrl})\n\nBest,\nMothership` const html = result.success ? await renderEmailHtml(result.content, chatUrl) : await renderErrorHtml(result.error || 'Unknown error', chatUrl) try { const response = await agentmail.replyToMessage( ctx.inboxProviderId, inboxTask.agentmailMessageId, { text, html } ) logger.info('Inbox response sent', { taskId: inboxTask.id, responseId: response.message_id }) return response.message_id } catch (error) { logger.error('Failed to send inbox response email', { taskId: inboxTask.id, error: getErrorMessage(error, 'Unknown error'), }) return null } } const FONT_FAMILY = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto, sans-serif" const CODE_FONT_FAMILY = "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace" const emailStyles = { body: { fontFamily: FONT_FAMILY, fontSize: '15px', lineHeight: '25px', color: '#1a1a1a', fontWeight: 430, }, content: { margin: 0, }, markdownContainer: { margin: 0, }, signature: { color: '#525252', marginTop: '32px', fontSize: '14px', }, signatureText: { color: '#525252', margin: '0 0 16px 0', fontSize: '14px', lineHeight: '25px', fontFamily: FONT_FAMILY, }, signatureLink: { color: '#1a1a1a', textDecoration: 'underline', textDecorationStyle: 'dashed', textUnderlineOffset: '2px', }, } satisfies Record const markdownStyles = { p: { margin: '0 0 16px 0', fontSize: '15px', lineHeight: '25px', color: '#1a1a1a', fontFamily: FONT_FAMILY, fontWeight: 430, }, h1: { fontWeight: 600, color: '#1a1a1a', margin: '24px 0 12px 0', fontSize: '24px', lineHeight: '32px', fontFamily: FONT_FAMILY, }, h2: { fontWeight: 600, color: '#1a1a1a', margin: '24px 0 12px 0', fontSize: '20px', lineHeight: '28px', fontFamily: FONT_FAMILY, }, h3: { fontWeight: 600, color: '#1a1a1a', margin: '24px 0 12px 0', fontSize: '16px', lineHeight: '24px', fontFamily: FONT_FAMILY, }, h4: { fontWeight: 600, color: '#1a1a1a', margin: '24px 0 12px 0', fontSize: '15px', lineHeight: '25px', fontFamily: FONT_FAMILY, }, strong: { fontWeight: 600, color: '#1a1a1a', }, codeInline: { backgroundColor: '#f3f3f3', padding: '2px 6px', borderRadius: '4px', fontFamily: CODE_FONT_FAMILY, fontSize: '13px', color: '#1a1a1a', }, codeBlock: { backgroundColor: '#f3f3f3', padding: '16px', borderRadius: '8px', border: '1px solid #ededed', overflowX: 'auto', margin: '24px 0', fontFamily: CODE_FONT_FAMILY, fontSize: '13px', lineHeight: '21px', color: '#1a1a1a', }, table: { borderCollapse: 'collapse', margin: '16px 0', }, th: { border: '1px solid #ededed', padding: '8px 12px', textAlign: 'left', fontSize: '14px', backgroundColor: '#f5f5f5', fontWeight: 600, }, td: { border: '1px solid #ededed', padding: '8px 12px', textAlign: 'left', fontSize: '14px', }, blockQuote: { borderLeft: '4px solid #e0e0e0', margin: '16px 0', padding: '4px 16px', color: '#525252', fontStyle: 'italic', }, a: { color: '#2563eb', textDecoration: 'underline', textDecorationStyle: 'dashed', textUnderlineOffset: '2px', }, ul: { margin: '16px 0', paddingLeft: '24px', }, ol: { margin: '16px 0', paddingLeft: '24px', }, li: { margin: '4px 0', }, hr: { border: 'none', borderTop: '1px solid #ededed', margin: '24px 0', }, } satisfies Record interface InboxResponseEmailProps { children?: ReactNode chatUrl: string linkLabel: string } interface EmailMarkdownProps { children?: string markdownContainerStyles?: CSSProperties markdownCustomStyles?: Record } const EmailMarkdown = Markdown as ComponentType function InboxResponseEmail({ children, chatUrl, linkLabel }: InboxResponseEmailProps) { return createElement( Html, { lang: 'en', dir: 'ltr' }, createElement(Head), createElement( Body, { style: emailStyles.body }, createElement(Section, { style: emailStyles.content }, children), createElement( Section, { style: emailStyles.signature }, createElement( Text, { style: emailStyles.signatureText }, createElement(Link, { href: chatUrl, style: emailStyles.signatureLink }, linkLabel) ), createElement( Text, { style: emailStyles.signatureText }, 'Best,', createElement('br'), 'Sim' ) ) ) ) } function stripRawHtml(text: string): string { return text .split(/(```[\s\S]*?```)/g) .map((segment, i) => i % 2 === 0 ? replaceUntilStable(segment, /<\/?[a-z][^>]*>/gi, '') : segment ) .join('') } function preserveSoftBreaks(text: string): string { return text .split(/(```[\s\S]*?```)/g) .map((segment, i) => (i % 2 === 0 ? segment.replace(/([^\n])\n(?=[^\n])/g, '$1 \n') : segment)) .join('') } function stripUnsafeUrls(html: string): string { return html.replace(/href\s*=\s*(['"])(?:javascript|vbscript|data):.*?\1/gi, 'href="#"') } async function renderEmailHtml(markdown: string, chatUrl: string): Promise { const safeMarkdown = preserveSoftBreaks(stripRawHtml(markdown)) const html = await render( createElement( InboxResponseEmail, { chatUrl, linkLabel: 'View full conversation' }, createElement( EmailMarkdown, { markdownContainerStyles: emailStyles.markdownContainer, markdownCustomStyles: markdownStyles, }, safeMarkdown ) ) ) return stripUnsafeUrls(html) } async function renderErrorHtml(error: string, chatUrl: string): Promise { const html = await render( createElement( InboxResponseEmail, { chatUrl, linkLabel: 'View details' }, createElement( Text, { key: 'message', style: markdownStyles.p }, "I wasn't able to complete this task." ), createElement( Text, { key: 'error', style: { ...markdownStyles.p, color: '#6b7280' } }, `Error: ${error}` ) ) ) return stripUnsafeUrls(html) }