HTML email generator, from a builder shaped for the wrong runtime
Every other guide on this topic ranks visual email builders. This one is about a different question: what happens when you point a general-purpose AI app builder at an email template, and why the answer is more interesting than "it works" or "it doesn't". The conflict is anchored in three lines of source. Once you can name them, you can override them.
HTML email is its own runtime
The phrase "HTML email" sounds like a mild dialect of HTML you read in a browser. It is not. It is a parallel render target with a different parser, a different layout engine, and a different set of forbidden features per client. Outlook for Windows uses Microsoft Word's rendering engine, which has no flex, no grid, no transform, and a frosty relationship with anything more recent than CSS 2.1. Gmail's web client cuts the message off at about 102 KB and strips most <style> blocks before display. Yahoo and AOL each have their own quirks. Apple Mail is the most permissive of the bunch, and even there a forced dark-mode pass can recolor your brand on you.
The shape that survives all of those is, basically, 2003 markup. A single outer table at a fixed width, nested tables for sections, inline style="..." on everything, system-font stacks, colors as hex literals, layout via padding rather than margin where possible, and one careful @media (max-width:600px) block for the phone breakpoint. That language is the lowest common denominator across thirty years of email clients. It is also exactly the language a modern frontend stack tries hard to abstract away.
Which is fine when you are using a tool built for email. Beefree, Stripo, Topol, Postcards, MJML, Mailmeteor, Unlayer, Sender: all built on this premise. They emit table-based, inline-styled HTML by default because they were designed around the constraints. A general-purpose AI app builder was not.
What email rendering actually demands
The pipeline below is what a single email goes through between sender and pixel. Every link in this chain is hostile to modern web assumptions. That is the gap an AI app builder has to clear.
HTML email render pipeline
Email client
Word engine on Outlook, sandboxed iframe in Gmail
Inline-only CSS
<style> often stripped
Table layout
no flex, no grid, fixed widths
VML fallbacks
background images on Outlook
Render
or your design collapses
What mk0r actually has under the hood
When you open mk0r and start typing, an E2B sandbox is already warm. Inside it, the project at /app exists before you arrive. It exists because docker/e2b/e2b.Dockerfile line 70 ran this command at template build time:
RUN npm create vite@latest . -- --template react-ts \&& npm install \&& npm install -D tailwindcss @tailwindcss/vite
So every fresh sandbox boots with a Vite + React + TypeScript + Tailwind v4 project on disk and a dev server already running on port 5173 with HMR. The agent reads two more files on first prompt: /app/CLAUDE.mdline 14 ("all new components must be imported here", pointing at src/App.tsx) and the system prompt at src/core/e2b.tsline 148, which describes the agent as "an expert app builder inside an E2B sandbox with a Vite + React + TypeScript + Tailwind CSS v4 project at /app".
Three reinforcing signals, all pointing at React. The agent does not pick the stack; the environment picks it. So when the prompt arrives, the path of least resistance is to add a component to src/App.tsx and call it done. That component might look great in a browser. It will fall apart in Outlook.
The same idea, in two languages
On the left is what the agent ships when you say "build me an order-confirmation email". It looks correct in the dev server preview because the dev server is a browser. The right side is the same content reshaped for an email client: nested tables, inline styles, fixed widths, no flex anywhere. Both render "the same email" if you squint. Only one survives Gmail and Outlook.
Modern HTML vs email-safe HTML
<!-- what the agent ships by default -->
<div className="flex items-center gap-4 p-6 bg-white rounded-xl">
<img src="/logo.svg" className="h-10 w-10" />
<div className="flex-1">
<h1 className="text-2xl font-bold text-zinc-900">
Order confirmed
</h1>
<p className="text-zinc-600 mt-1">
Thanks, {firstName}. Your order is on its way.
</p>
</div>
<a
href={ctaUrl}
className="inline-block bg-teal-500 hover:bg-teal-400
text-white px-5 py-2.5 rounded-lg font-medium"
>
Track order
</a>
</div>The right-hand version is verbose on purpose. There is no shorthand. Every padding has to live on a <td>, every color has to be a hex literal in an inline style, every CTA button has to be a table cell with a bgcolor attribute because Outlook ignores rounded corners on links. The placeholders ({{ first_name }} and similar) are intentionally left as Liquid-style tokens so a templating engine can fill them at send time.
What each major client actually breaks on
None of this is the agent's fault when it goes wrong. The quirks below are what the email runtime imposes. The point of knowing them is that you can spell them out in the prompt, and the agent will respect each one if you do.
Outlook for Windows
Renders with Word's HTML engine. Drops flex, grid, gap, transform, and most modern selectors. Floats and tables work. Background images on table cells need a VML fallback wrapped in <!--[if mso]> conditional comments or they vanish.
Gmail (web)
Strips <style> blocks aggressively when the message exceeds about 102 KB and clips the rest. Inline style attributes survive intact. CSS custom properties get dropped. Class selectors only fire if the <style> survives the strip.
Apple Mail
The most permissive of the major clients. Renders most modern CSS, but assume the user is on a phone in dark mode and on a slow network. Set explicit dark-mode color overrides because forced dark inversion will recolor your brand.
Yahoo and AOL
Quirks closer to Gmail than to Outlook. Embedded <style> sometimes survives, sometimes not. Webfonts via @import get blocked. Inline + a system font stack is the safe default for body text.
The override prompt that produces a real email
A prompt that gets a usable email out of a Vite-shaped sandbox does five things at once. Drop any one and the React scaffold pulls the agent back to the path of least resistance.
- Name the output path. "Write to
/app/dist/order-confirm.html, do not edit src/." The path tells the agent this artifact lives outside the React tree. Without it, the file lands next to App.tsx and you spend ten minutes finding it. - Forbid the modern stack out loud. "No React, no Tailwind, no flex, no grid, no @import, no external stylesheets, no script tags, no class attributes." This list is the agent's prior, written backwards. Each item cancels one of the defaults the sandbox is shaped to encourage.
- Spell out the email shape. "Single 600px-wide outer table, role="presentation", cellpadding=0 cellspacing=0 border=0, inline every style attribute, hex colors only, system-font stacks. CTA button is a
<td bgcolor>with an<a>inside." This is the email runtime, made explicit. Most of the agent's training data on HTML is web HTML, so the constraints have to be in the prompt or they get optimized away. - Pin the placeholders. "Use {{ first_name }} and {{ cta_url }}as literal tokens. Do not interpolate them at generation time." Without this the agent fills the tokens with example values and you get a one-off email instead of a template.
- Force file:// verification. "After writing, open file:///app/dist/order-confirm.html in the in-VM browser, screenshot, then send a test copy to you@example.com via Resend." This bypasses the dev-server preview, which only knows about the React project. file:// is the actual artifact.
Glued together it is verbose, maybe 200 words. That is the price of crossing runtimes. Saved as a reusable starter prompt, it pays back on every email after the first.
Where the same sandbox closes the loop
Generating the HTML is half the job. The other half is sending it, tracking opens, storing the recipient list. A drag-and-drop email builder hands you the HTML; you still need to glue it into an ESP, a database, and an admin UI. The mk0r sandbox skips that step, because the same /app/CLAUDE.md that pushes the agent toward React also lists Resend and Neon as pre-provisioned services on lines 43-46:
“Resend | Transactional email | RESEND_API_KEY, RESEND_AUDIENCE_ID”
docker/e2b/files/app/CLAUDE.md
Which means once you have order-confirm.htmlon disk, the next prompt is one sentence: "Read /app/dist/order-confirm.html, replace the placeholders with values from recipients.json, and call resend.emails.send for each row. Log every send into a Neon emails table." The agent has shell access, the env var is already set, the database is already provisioned. No accounts to create, no API keys to paste. The generate-and-send loop happens inside one box.
That generate-store-send shape is where a general AI app builder beats a dedicated email tool. The dedicated tool gives you better templates and a friendlier WYSIWYG. The general tool gives you the whole pipeline in one prompt thread.
Honest limits
For anything more complex than a transactional template, a real email-specific tool wins. Beefree, Stripo, and Postcards have template libraries in the thousands and a visual editor that knows the constraints natively. MJML lets you write structured templates in JSX-like syntax that compiles down to safe HTML. If you are sending high-volume marketing campaigns with weekly redesigns and a non-technical team, that workflow beats prompting an AI agent.
The general-builder path is best when you want one HTML email plus the send infrastructure plus a tiny admin page in the same evening, you are comfortable reading the HTML the agent produces, and the email you need is small enough to write by hand once you know the shape. Order confirmations, password resets, weekly digests for a small list, beta-invite blasts: all easy. A 12-section newsletter with seven A/B variants and a dark-mode override per section: still possible, but you will spend the time in MJML or a builder anyway.
Both paths are valid. The trap is asking a general AI app builder for an email and accepting the React component it ships first, because the dev-server preview looks correct. The override prompt above is what closes that gap. The pre-provisioned send pipeline is what makes the trade worth taking.
Want a teardown of your email-generation prompt?
Fifteen minutes. Bring an email idea, leave with a working .html that survives Gmail and Outlook plus a prompt you can reuse for the next one.
Frequently asked questions
Why isn't a general AI app builder a good HTML email generator out of the box?
Because the sandbox is shaped for the wrong runtime. On mk0r, docker/e2b/e2b.Dockerfile line 70 runs `npm create vite@latest . -- --template react-ts` at template build time. Every fresh sandbox already has a Vite dev server on port 5173, a src/App.tsx waiting for components, and Tailwind CSS v4 wired up. Email clients render none of that. Gmail strips most <style> blocks unless they sit inline, Outlook for Windows uses Word's rendering engine and ignores flexbox and grid, and many clients block external CSS entirely. The agent's environment is shaped for the browser; HTML email is a different runtime.
What kinds of HTML does an email client actually accept reliably?
Tables for layout, inline `style="..."` attributes on every element that needs styling, fixed widths in pixels, and old-school attributes like `cellpadding`, `cellspacing`, and `border="0"` on tables. Background images often need a VML fallback for Outlook. Modern CSS (flex, grid, custom properties, @media beyond simple max-width breakpoints, gap, aspect-ratio, calc with mixed units) is roulette. The lowest-common-denominator email looks like 2003 markup on purpose. That is what travels intact through Gmail, Outlook, Yahoo, Apple Mail, and the long tail of corporate clients.
Can mk0r still produce a working HTML email if I ask the right way?
Yes. The agent has shell access at /app and can write any file at any path. The override has three pieces. First, name the output path explicitly: 'write the email to /app/dist/email.html, do not edit src/'. Second, forbid the modern stack out loud: 'no React, no Tailwind, no flex, no grid, no @import, no external stylesheets, no script tags'. Third, force the email constraints in: 'use a single 600px-wide table for the outer container, inline every style attribute, set cellpadding/cellspacing/border on the table, use only @media (max-width:600px) for mobile'. Without all three, the agent drifts back to a React component because that is the path of least resistance in the sandbox.
What is the cheapest way to test the email actually renders before sending it?
Open it in three places: a local browser via file:///app/dist/email.html, a real Gmail inbox by sending a copy to yourself with the pre-provisioned Resend API key, and Outlook on Windows if you have access. The browser tells you the table structure and colors are right. Gmail tells you whether the inline styles survived. Outlook tells you whether the layout collapses. Tools like Litmus and Email on Acid render across dozens of clients but are paid; for a small project the three-client check catches 90 percent of real breakage. The quick gut-check inside the mk0r sandbox is: ask the agent to navigate the in-VM Chromium to the file:// URL, then to send via Resend to your own inbox, then screenshot Gmail. That is one prompt.
How is this different from MJML or a drag-and-drop tool like Beefree or Stripo?
MJML is a templating language that compiles to email-safe HTML, which is fantastic if you want to write structured templates by hand. Beefree, Topol, Stripo, Postcards, and Unlayer are visual builders: you drag blocks, you export HTML. Both categories are purpose-built for the email runtime; they win on default correctness and template libraries. A general AI app builder like mk0r wins on a different axis: the same sandbox that writes the HTML can also send it (Resend is pre-provisioned per CLAUDE.md line 45), can store the contact list (Neon Postgres is also pre-provisioned), and can stand up a tiny landing page for the campaign in one session. The trade is that you have to override the default scaffold to keep the email itself email-safe.
Where does the agent's email default actually come from in the source?
Three places, all loud. src/core/e2b.ts line 148 sets the system prompt: 'an expert app builder inside an E2B sandbox with a Vite + React + TypeScript + Tailwind CSS v4 project at /app. The dev server is running on port 5173 with HMR.' That is the agent's prior. docker/e2b/files/app/CLAUDE.md line 14 reads 'all new components must be imported here' pointing at src/App.tsx, so the local rules tell the agent to add components to a React root. docker/e2b/e2b.Dockerfile line 70 baked the Vite scaffold into the template image at build time, so the project on disk already exists before the agent reads either file. Three reinforcing signals, all pointing at React. To get a hand-written HTML email, you have to override all three with one prompt.
Does mk0r have email infrastructure I can actually use, or is it just generation?
Yes, and that is the part most reviews of AI app builders miss for this topic. docker/e2b/files/app/CLAUDE.md line 45 lists Resend as a pre-provisioned service with `RESEND_API_KEY` and `RESEND_AUDIENCE_ID` already set in .env. Same file lists Neon Postgres for storage. So once the agent has produced a clean HTML email, the same sandbox can call resend.emails.send to deliver it, write the recipient and subject into a Postgres `emails` table, and expose a tiny endpoint that triggers the send. That generate-write-send loop in one box is what a drag-and-drop builder cannot do; you still need a separate ESP, a separate database, and glue.
What about dynamic content (per-recipient name, links, dates) in the generated email?
Two paths, depending on volume. For a small list, ask the agent to write a Node script in the same sandbox that reads recipients from a JSON file or a Neon table, does string replacement on placeholders like {{first_name}} in the email HTML, and calls Resend per recipient. That is a five-minute job once the email exists. For larger volumes you want Resend's broadcast feature plus their Liquid-style merge tags, or migrate the template to MJML and use a real templating engine. The agent can do either; the constraint is just: make the placeholder syntax explicit in the prompt so the rendered HTML keeps the tokens intact instead of resolving them at generation time.
Is there a single shortest prompt that gets a usable HTML email out of mk0r?
Something like: 'Build a transactional email at /app/dist/order-confirm.html. Single 600px-wide table, cellpadding=0 cellspacing=0 border=0, inline every style attribute, no external CSS, no flex, no grid, no script. Logo + heading + order summary table + CTA button (use a table for the button) + footer. Mobile: one @media (max-width:600px) block at the top, set width: 100% on the outer table. After writing, open file:///app/dist/order-confirm.html in the in-VM browser, screenshot, then send a test copy to my address via Resend.' Five constraints, one verification step. Drop any constraint and the React scaffold pulls the agent back.
Adjacent reading
Single HTML File App
The sibling case to this page: how to flip the sandbox out of multi-file React mode and into a hand-written single HTML file you can ship.
AI App Builder Database Persistence
Once the email is sending, the recipient list lives somewhere. This is what 'storage' actually means inside the same sandbox.
AI App Builder With Backend
Resend is one of four pre-provisioned services. The full picture of what is wired into every sandbox before you type the first prompt.