Skip to content
// Gmail · Apps Script

Send an HTML email in Gmail.

How to send a properly formatted HTML email from Google Apps Script using GmailApp.sendEmail — including the plain-text fallback that prevents clients from showing raw markup.

I want to send an email from Apps Script that shows formatted HTML instead of raw markup, but I'm not sure where to put the HTML or why my tags are showing up as literal text.

The script

copy · paste · trigger
sendHtmlEmail.gs
Apps Script
// Send an HTML email with a plain-text fallback
// Run sendHtmlEmail() from the Apps Script editor to test

function sendHtmlEmail() {
  var recipient = '[email protected]';
  var subject  = 'Your weekly report';

  var plainBody = 'Weekly report attached. Open in a modern client to see formatting.';

  var htmlBody =
    '<h2>Weekly Report</h2>' +
    '<p>Here is your summary for the week:</p>' +
    '<ul>' +
    '<li>Tasks completed: <strong>12</strong></li>' +
    '<li>Open items: <strong>3</strong></li>' +
    '</ul>' +
    '<p><a href="https://example.com/report">View full report</a></p>';

  GmailApp.sendEmail(recipient, subject, plainBody, { htmlBody: htmlBody });
}

Need a variant? Gnaw writes a custom version from one sentence — fields, triggers, edge cases handled.

Walkthrough

The third argument is not your HTML

GmailApp.sendEmail takes four arguments: recipient, subject, body, and an options object. That third positional argument, body, is the plain-text version of the email. Put HTML there and recipients whose clients strip or block HTML will see raw tags in their inbox.

Your HTML goes in options.htmlBody. When a mail client supports HTML, it renders that string. When it does not, or when a user has images and rich content disabled, the client falls back to body. Both fields are required for a well-formed message.

The first time I hit this, I was putting a full HTML string as the third argument and wondering why Gmail itself was showing tags. The fix is one extra object literal: GmailApp.sendEmail(to, subject, plainText, { htmlBody: myHtml }).

The plain-text fallback does not need to be a stripped version of the HTML. A short summary sentence is enough. Its job is to survive the rare client or filter that refuses all HTML, not to duplicate the full message.

Quoting attributes inside concatenated strings

Because the HTML is built from regular JavaScript string literals, any attribute value in double quotes needs to be escaped as \" inside a double-quoted JS string, or you can alternate quote styles: put the JS string in single quotes and use double quotes freely inside the HTML.

The snippet above wraps each JS string in single quotes, so href="https://..." works without escaping. That is the easier habit to carry.

One thing to watch: angle brackets inside attribute values, like a URL with a query string containing &amp; need to be written as &amp;amp; if you want valid HTML. Gmail's renderer is forgiving, but downstream parsers in automated pipelines are not.

Daily quota and authorization scope

GmailApp.sendEmail counts against your Gmail sending quota: 100 emails per day for consumer accounts, 1,500 per day for Google Workspace accounts. Each call is one email regardless of recipient count; adding multiple addresses to the to field still costs one quota unit.

The first time the script runs, Apps Script will ask you to authorize the Gmail scope (https://mail.google.com/). If the script is attached to a Workspace account and you plan to run it on a trigger, authorize it manually once first by running it from the editor so the OAuth token is stored before the trigger fires.

MailApp.sendEmail uses a smaller, less permissive scope and the same quota semantics. If you only need to send and never need to read or manage mail, MailApp is the correct choice. GmailApp is worth the broader scope only when the same script also reads threads or labels.

Want a custom version?

Describe your sheet and the rule you want. Gnaw writes the Apps Script — fields, triggers, edge cases — in one shot.

FAQ

4 questions
Why does my email show HTML tags instead of formatted content?
You put the HTML string in the third argument (body) instead of options.htmlBody. The third argument is the plain-text fallback; it renders literally. Move your HTML to the options object: GmailApp.sendEmail(to, subject, plainText, { htmlBody: yourHtml }).
Do I need the plain-text body if I'm providing htmlBody?
Yes. The third argument is required by the API and cannot be an empty string in all contexts. A one-sentence summary is enough. Clients that block HTML will show that string; clients that render HTML will ignore it.
Can I use inline CSS styles in the HTML?
Yes, and you should. Most email clients, including Gmail's own iOS and Android apps, strip or ignore linked stylesheets and style blocks in the head. Write all styles inline on each element (style="color: #333; font-size: 14px;") for consistent rendering across clients.
What is the difference between GmailApp.sendEmail and MailApp.sendEmail?
MailApp requires only the mail.google.com/send scope, which is narrower. GmailApp requires full mail access. Both share the same daily quota (100 consumer, 1500 Workspace). Use MailApp when the script only sends; use GmailApp when it also reads or searches threads in the same execution.
// one good script a week

Get a working Apps Script snippet in your inbox, weekly.