import {useEffect, useState} from 'react' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {Loader2, Mail, Plus, RefreshCw, Send, Trash2} from 'lucide-react' import {createSesConfigurationSet, createSesEventDestination, createSesIdentity, deleteSesConfigurationSet, deleteSesEventDestination, deleteSesIdentity, deleteSesSuppression, deleteSesTemplate, getSesTemplate, listSesConfigurationSets, listSesEventBuses, listSesEventDestinations, listSesIdentities, listSesSuppressions, listSesTemplates, listSesTopics, putSesSuppression, renderSesTemplate, saveSesTemplate, sendSesRaw, sendSesSimple, sendSesTemplate} from '@/api/sesClient' const SIMULATORS = ['success@simulator.amazonses.com', 'bounce@simulator.amazonses.com', 'complaint@simulator.amazonses.com'] const EVENT_TYPES = ['SEND', 'DELIVERY', 'BOUNCE', 'COMPLAINT', 'REJECT'] export function SesConsolePanel({runtimeReachable}: {runtimeReachable: boolean}) { const qc = useQueryClient() const [identity, setIdentity] = useState('') const [mode, setMode] = useState<'simple' | 'raw' | 'template'>('simple') const [from, setFrom] = useState('') const [to, setTo] = useState(SIMULATORS[0]) const [subject, setSubject] = useState('Local SES test') const [text, setText] = useState('Sent from TheCloud\'s SES console.') const [html, setHtml] = useState('') const [raw, setRaw] = useState('From: sender@example.test\r\nTo: success@simulator.amazonses.com\r\nSubject: Raw SES test\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\nHello from a raw MIME message.') const [configurationSet, setConfigurationSet] = useState('') const [templateName, setTemplateName] = useState('') const [templateData, setTemplateData] = useState('{"name":"TheCloud"}') const [editingTemplate, setEditingTemplate] = useState('') const [templateSubject, setTemplateSubject] = useState('Hello {{name}}') const [templateText, setTemplateText] = useState('Hello {{name}}') const [templateHtml, setTemplateHtml] = useState('') const [rendered, setRendered] = useState('') const [suppressionAddress, setSuppressionAddress] = useState('') const [suppressionReason, setSuppressionReason] = useState<'BOUNCE' | 'COMPLAINT'>('BOUNCE') const [configurationSetName, setConfigurationSetName] = useState('') const [selectedConfigurationSet, setSelectedConfigurationSet] = useState('') const [destinationName, setDestinationName] = useState('') const [destinationKind, setDestinationKind] = useState<'sns' | 'eventbridge' | 'cloudwatch'>('sns') const [destinationTarget, setDestinationTarget] = useState('') const [events, setEvents] = useState(['SEND', 'BOUNCE', 'DELIVERY']) const [dimensionName, setDimensionName] = useState('ses:configuration-set') const identities = useQuery({queryKey: ['ses-identities'], queryFn: listSesIdentities, enabled: runtimeReachable}) const templates = useQuery({queryKey: ['ses-templates'], queryFn: listSesTemplates, enabled: runtimeReachable}) const suppressions = useQuery({queryKey: ['ses-suppressions'], queryFn: listSesSuppressions, enabled: runtimeReachable}) const configurationSets = useQuery({queryKey: ['ses-configuration-sets'], queryFn: listSesConfigurationSets, enabled: runtimeReachable}) const topics = useQuery({queryKey: ['ses-topics'], queryFn: listSesTopics, enabled: runtimeReachable}) const buses = useQuery({queryKey: ['ses-event-buses'], queryFn: listSesEventBuses, enabled: runtimeReachable}) const destinations = useQuery({queryKey: ['ses-event-destinations', selectedConfigurationSet], queryFn: () => listSesEventDestinations(selectedConfigurationSet), enabled: runtimeReachable && Boolean(selectedConfigurationSet)}) const refreshMailbox = () => qc.invalidateQueries({queryKey: ['cloud-resources', 'aws', 'email']}) const send = useMutation({mutationFn: async () => { const destination = {ToAddresses: to.split(',').map((value) => value.trim()).filter(Boolean)} const options = configurationSet ? {ConfigurationSetName: configurationSet} : {} if (mode === 'raw') return sendSesRaw({...options, FromEmailAddress: from || undefined, Destination: destination, Content: {Raw: {Data: btoa(unescape(encodeURIComponent(raw)))}}}) if (mode === 'template') return sendSesTemplate({...options, FromEmailAddress: from, Destination: destination, Content: {Template: {TemplateName: templateName, TemplateData: templateData}}}) return sendSesSimple({...options, FromEmailAddress: from, Destination: destination, Content: {Simple: {Subject: {Data: subject}, Body: {...(text ? {Text: {Data: text}} : {}), ...(html ? {Html: {Data: html}} : {})}}}}) }, onSuccess: () => { refreshMailbox(); setTimeout(refreshMailbox, 300) }}) const createIdentity = useMutation({mutationFn: () => createSesIdentity(identity.trim()), onSuccess: () => {setIdentity(''); void qc.invalidateQueries({queryKey: ['ses-identities']})}}) const saveTemplate = useMutation({mutationFn: () => saveSesTemplate(editingTemplate.trim(), {Subject: templateSubject, ...(templateText ? {Text: templateText} : {}), ...(templateHtml ? {Html: templateHtml} : {})}, templates.data?.some((item) => item.TemplateName === editingTemplate) ?? false), onSuccess: () => void qc.invalidateQueries({queryKey: ['ses-templates']})}) const renderTemplate = useMutation({mutationFn: () => renderSesTemplate(editingTemplate || templateName, templateData), onSuccess: setRendered}) const addSuppression = useMutation({mutationFn: () => putSesSuppression(suppressionAddress.trim(), suppressionReason), onSuccess: () => {setSuppressionAddress(''); void qc.invalidateQueries({queryKey: ['ses-suppressions']})}}) const createConfigurationSet = useMutation({mutationFn: () => createSesConfigurationSet(configurationSetName.trim()), onSuccess: () => {setConfigurationSetName(''); void qc.invalidateQueries({queryKey: ['ses-configuration-sets']})}}) const addDestination = useMutation({mutationFn: () => createSesEventDestination(selectedConfigurationSet, {EventDestinationName: destinationName.trim(), EventDestination: {Enabled: true, MatchingEventTypes: events, ...(destinationKind === 'sns' ? {SnsDestination: {TopicArn: destinationTarget}} : destinationKind === 'eventbridge' ? {EventBridgeDestination: {EventBusArn: destinationTarget}} : {CloudWatchDestination: {DimensionConfigurations: [{DimensionName: dimensionName, DimensionValueSource: 'MESSAGE_TAG', DefaultDimensionValue: 'local'}]}})}}), onSuccess: () => {setDestinationName(''); void qc.invalidateQueries({queryKey: ['ses-event-destinations', selectedConfigurationSet]})}}) const error = send.error ?? createIdentity.error ?? saveTemplate.error ?? renderTemplate.error ?? addSuppression.error ?? createConfigurationSet.error ?? addDestination.error useEffect(() => { if (!selectedConfigurationSet && configurationSets.data?.[0]?.ConfigurationSetName) setSelectedConfigurationSet(configurationSets.data[0].ConfigurationSetName) }, [configurationSets.data, selectedConfigurationSet]) async function loadTemplate(name: string) { setEditingTemplate(name); const detail = await getSesTemplate(name); setTemplateSubject(detail.TemplateContent?.Subject ?? ''); setTemplateText(detail.TemplateContent?.Text ?? ''); setTemplateHtml(detail.TemplateContent?.Html ?? '') } const pending = send.isPending || createIdentity.isPending || saveTemplate.isPending || addSuppression.isPending || createConfigurationSet.isPending || addDestination.isPending return

SES identities

Verify senders

TheCloud verifies identities immediately for AWS-compatible workflows. Local sending is not sandbox-gated.

{event.preventDefault(); if (identity.trim()) createIdentity.mutate()}}> setIdentity(event.target.value)} placeholder="sender@example.test or example.test" required/>
{identities.data?.map((item) =>
{item.IdentityName}{item.IdentityType} · {item.VerifiedForSendingStatus ? 'verified for sending' : 'not verified'}
)}

Compose

Send through SES

Simulator recipients produce documented delivery, bounce, and complaint events locally.

{SIMULATORS.map((address) => )}
{event.preventDefault(); send.mutate()}}>{mode === 'simple' && <>