/// /** * Gmail Sender Pattern * * Sends emails via Gmail API with mandatory user confirmation. * * Security: User must see the exact email content and explicitly confirm * before any email is sent. This pattern can serve as a declassification * gate when policies are implemented (patterns with verified SHA can be trusted). * * Usage: * 1. Create and favorite a Google Auth piece with "Gmail (send emails)" permission * 2. Create a Gmail Sender piece * 3. Compose your email and click "Review & Send" * 4. Review the confirmation dialog showing exactly what will be sent * 5. Click "Send Email" to send * * Multi-account support: Use createGoogleAuth() with accountType parameter * to wish for #googleAuthPersonal or #googleAuthWork accounts. * See: gmail-importer.tsx for an example with account switching dropdown. */ import { computed, Default, handler, ifElse, NAME, pattern, UI, type VNode, Writable, } from "commontools"; import { GmailSendClient } from "../util/gmail-send-client.ts"; import { type Auth, createGoogleAuth } from "../util/google-auth-manager.tsx"; // ============================================================================ // TYPES // ============================================================================ type EmailDraft = { /** Recipient email address */ to: Default; /** Email subject line */ subject: Default; /** Plain text body */ body: Default; /** CC recipients (comma-separated) */ cc: Default; /** BCC recipients (comma-separated) */ bcc: Default; /** Message ID to reply to (for threading) */ replyToMessageId: Default; /** Thread ID to reply to (for threading) */ replyToThreadId: Default; }; type SendResult = { success: boolean; messageId?: string; threadId?: string; error?: string; timestamp?: string; }; interface Input { /** Email draft to compose/send */ draft: Default< EmailDraft, { to: ""; subject: ""; body: ""; cc: ""; bcc: ""; replyToMessageId: ""; replyToThreadId: ""; } >; } /** Gmail email sender with confirmation dialog. #gmailSender */ interface Output { [UI]: VNode; draft: EmailDraft; result: SendResult | null; } // ============================================================================ // HANDLERS // ============================================================================ const prepareToSend = handler< unknown, { showConfirmation: Writable } >((_, { showConfirmation }) => { showConfirmation.set(true); }); const cancelSend = handler< unknown, { showConfirmation: Writable } >((_, { showConfirmation }) => { showConfirmation.set(false); }); const confirmAndSend = handler< unknown, { draft: Writable; auth: Writable; sending: Writable; result: Writable; showConfirmation: Writable; } >(async (_, { draft, auth, sending, result, showConfirmation }) => { sending.set(true); result.set(null); try { const client = new GmailSendClient(auth, { debugMode: true }); const email = draft.get(); const response = await client.sendEmail({ to: email.to, subject: email.subject, body: email.body, cc: email.cc || undefined, bcc: email.bcc || undefined, replyToMessageId: email.replyToMessageId || undefined, replyToThreadId: email.replyToThreadId || undefined, }); result.set({ success: true, messageId: response.id, threadId: response.threadId, timestamp: new Date().toISOString(), }); showConfirmation.set(false); // Clear draft on success draft.set({ to: "", subject: "", body: "", cc: "", bcc: "", replyToMessageId: "", replyToThreadId: "", }); } catch (error) { result.set({ success: false, error: error instanceof Error ? error.message : String(error), timestamp: new Date().toISOString(), }); } finally { sending.set(false); } }); const dismissResult = handler }>( (_, { result }) => { result.set(null); }, ); // ============================================================================ // PATTERN // ============================================================================ export default pattern(({ draft }) => { // Auth via createGoogleAuth - discovers favorited Google Auth piece with gmailSend scope const { auth, fullUI: authUI, isReady: hasAuth, currentEmail: senderEmail, } = createGoogleAuth({ requiredScopes: ["gmailSend"], }); // UI state const showConfirmation = Writable.of(false); const sending = Writable.of(false); const result = Writable.of(null); // Validation const canSend = computed(() => hasAuth && draft.to.trim() !== "" && draft.subject.trim() !== "" && draft.body.trim() !== "" && !sending.get() ); return { [NAME]: "Gmail Sender", [UI]: (

Send Email

{/* Auth status - using createGoogleAuth UI with avatar and switch button */} {authUI} {/* Result display */} {ifElse( computed(() => result.get()?.success === true),
Email Sent Successfully!
Message ID: {computed(() => result.get()?.messageId)}
, null, )} {ifElse( computed(() => result.get()?.success === false),
Failed to Send Email
{computed(() => result.get()?.error)}
, null, )} {/* Compose form */}
{/* CONFIRMATION DIALOG */} {ifElse( showConfirmation,
{/* Header */}
📧

Confirm Send Email

{/* Content */}
From: {senderEmail}
To: {draft.to}
{ifElse( computed(() => draft.cc && draft.cc.trim() !== ""),
CC: {draft.cc}
, null, )} {ifElse( computed(() => draft.bcc && draft.bcc.trim() !== ""),
BCC: {draft.bcc}
, null, )}
Subject: {draft.subject}
Message:
{draft.body}
{/* Warning */}
This will send a real email
The recipient will receive this email from your Google account. This action cannot be undone.
{/* Footer */}
, null, )}
), draft, result, }; });