///
/**
* 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),