mail-html-injector

Mail HTML Injector

A privacy-first Chrome extension (Manifest V3) that lets you insert, edit and preview custom HTML inside the Gmail compose window — built for newsletters and mailings.

Everything runs locally. No tracking, no analytics, no external requests, no remote code. Templates are stored in chrome.storage.local on your machine.

Features

Why vanilla JavaScript (no TypeScript, no build step)?

The project is deliberately build-free: what you clone is exactly what Chrome loads. TypeScript would add a compile step, a node_modules tree, and a dist/ folder for marginal benefit in a codebase of ~5 small files. With vanilla JS you edit a file, hit reload on chrome://extensions, refresh Gmail — done. The only third-party code is DOMPurify, vendored as a single minified file in lib/ (Manifest V3 forbids remote code anyway).

Project structure

mail-html-injector/            ← load THIS folder as unpacked extension
├── manifest.json              MV3 manifest (storage permission + mail.google.com only)
├── README.md
├── .gitignore
├── icons/
│   ├── icon16.png             placeholder icons (blue circle with "</>")
│   ├── icon48.png
│   └── icon128.png
├── lib/
│   └── purify.min.js          DOMPurify 3.1.7, bundled locally (no CDN)
└── src/
    ├── content/               content scripts, loaded in order by the manifest
    │   ├── sanitizer.js       DOMPurify wrapper (email-friendly config)
    │   ├── templates.js       template CRUD on chrome.storage.local
    │   ├── modal.js           editor modal (Shadow DOM, code + live preview)
    │   ├── content.js         MutationObserver + toolbar button injection
    │   └── button.css         toolbar button styles (mhi- prefixed)
    └── popup/
        ├── popup.html         extension popup: info + enable/disable
        ├── popup.css
        └── popup.js

CSS isolation

The editor modal lives entirely inside a closed Shadow DOM, so Gmail’s CSS cannot affect it and vice versa. The only styles injected into Gmail’s page are for the toolbar button, all prefixed mhi-.

Gmail DOM fragility

Gmail’s class names are obfuscated and change without notice. All selector dependencies are concentrated in src/content/content.js and flagged with GMAIL-DOM comments. The code prefers stable semantic anchors (div[g_editable="true"], [role="textbox"]) and uses obfuscated classes (.btC, td.gU.Up) only as placement hints, with a floating-button fallback if the toolbar can’t be found. If something breaks after a Gmail update, start there.

Local installation (no Chrome Web Store)

This extension is meant to run unpacked. It will never ask you to publish anything.

  1. Open Chrome and navigate to chrome://extensions (type it in the address bar).

  2. Enable Developer mode with the toggle in the top-right corner of the page.

  3. Click “Load unpacked” and select the project root folder — the mail-html-injector/ folder itself, the one that directly contains manifest.json. (Not src/, not any subfolder.)

  4. Open Gmail (https://mail.google.com) — or, if it was already open, reload the tab (content scripts only attach on page load). Click Compose and verify a blue </> button appears in the bottom toolbar of the compose window, next to Send.

  5. Troubleshooting:

    • Button doesn’t appear in the toolbar but a floating </> button appears over the message body → Gmail renamed its toolbar classes. The extension still works via the fallback button; to restore toolbar placement, inspect the compose toolbar with DevTools and update the GMAIL-DOM selectors in src/content/content.js (TOOLBAR_SELECTOR, td.gU.Up).
    • No button at all → check: (a) the Gmail tab was reloaded after installing/reloading the extension; (b) the extension is enabled both on chrome://extensions and in its own popup toggle; (c) open DevTools on the Gmail tab and look for [Mail HTML Injector] errors in the Console.
    • Gmail in another language → fully supported. The extension deliberately avoids language-dependent selectors like [aria-label="Message Body"] and anchors on g_editable/role attributes instead, which are identical in every locale.
    • Extension disabled after restarting Chrome → Chrome sometimes shows a “Disable developer mode extensions?” prompt at startup; click Keep / dismiss it. If the extension was turned off, re-enable it on chrome://extensions. Unpacked extensions also break if the project folder is moved or renamed — reload it from its new location with “Load unpacked” again. (These prompts are the normal cost of unpacked extensions; there is no way around them without the Web Store.)

Usage

  1. Click Compose in Gmail.
  2. Click the </> button in the compose toolbar.
  3. The editor opens pre-loaded with the current body HTML. Paste or edit your HTML on the left; the sanitized preview renders live on the right.
  4. Optionally save it as a template (name + Save current HTML), or load a previously saved one.
  5. Click Replace body (typical for newsletters) or Insert at cursor.
  6. Send the email normally with Gmail’s Send button.

Permissions

Permission Why
storage Save your HTML templates locally (chrome.storage.local).
https://mail.google.com/* Inject the button/editor into Gmail. Nothing else.

No background service worker, no web requests, no other hosts.

Roadmap

License

Source-available, all rights reserved — see LICENSE.

You may download and use this extension for free, for personal use. You may not redistribute, republish, sell, or publish modified/unmodified copies without written permission. This is intentionally not an open-source license: the code is public so you can read and run it, not copy it.

DOMPurify (lib/purify.min.js) is © Cure53 and contributors, dual-licensed Apache 2.0 / MPL 2.0, and keeps its own license — see the header of that file.

Gmail and Google are trademarks of Google LLC. This project is not affiliated with, endorsed by, or sponsored by Google.