Inspiration

Have you ever hit Send and immediately noticed a typo in the subject line? Accidentally left a placeholder in the body? Forgotten to attach the file you mentioned? Sent a reply-all to the entire company when you meant to reply only to one person?

We've all been there. Email is one of the few digital actions that's truly irreversible — once it's gone, it's gone. We wanted to build something that addresses this without changing how people work. No new app to learn, no workflow to adopt. Just a quiet safety net that catches you at the exact moment you're most likely to make a mistake: the split second after you hit Send.

What We Learned

Chrome extension architecture is deceptively nuanced. Manifest V3 imposes strict content security policies, and content scripts must be classic IIFE bundles — no ESM imports allowed. Getting two separate build pipelines (Vite for the popup, esbuild for the content scripts) to cooperate inside a single pnpm build command taught us a lot about how modern bundlers hook into each other.

Gmail and Outlook are very different beasts. Gmail's DOM is heavily obfuscated with generated class names, so interception relies on ARIA attributes and structural patterns. Outlook fires events differently — pointerdown had to be intercepted in addition to click to reliably catch sends. Supporting both platforms meant writing two parallel interceptor stacks that share the same analysis and UI layer.

External APIs inside content scripts require careful handling. The LanguageTool API call races against the countdown timer, and the modal has to be fully functional even if the API times out or fails. We learned to build UI that degrades gracefully — showing a skeleton loader while analysis runs, then swapping in results, or a fallback message if the fetch never completes.

False positives erode trust fast. Early builds flagged "missing space after comma" on every email with a bulleted list, because LanguageTool treats the comma-newline boundary as a punctuation error. We wrote a filter pass to suppress these, which gave us a deeper understanding of the API's offset and context model.

How We Built It

Second Chance is a Manifest V3 Chrome extension written in TypeScript with strict mode enabled throughout.

When you hit Send, event listeners at the capture phase intercept the action and cancel it immediately. A WeakSet lock is placed on the compose window to prevent re-entry, and the compose context — recipients, subject, body, attachments — is extracted from the DOM. That context is passed to computePauseAnalysis, which fires the LanguageTool API and all local checks in parallel, then opens the countdown modal.

The modal shows a skeleton loader immediately and swaps in analysis results as they arrive — the countdown never waits for the API. When the user confirms (or the timer hits zero), a bypass marker attribute is set on the native send button before clicking it programmatically, so our own interceptor ignores the synthetic click and lets Gmail or Outlook handle the actual send.

Strict mode applies a ×1.35 multiplier on top, capped at 60 seconds. Four categories of checks run on every send: grammar via LanguageTool, formatting checked locally, tone via a severity-mapped profanity list with leet-normalization, and context warnings like recipient count and missing attachments.

Challenges

Reliable send interception without breaking Gmail. Gmail dispatches send events in ways that don't map cleanly to a single DOM event. Ensuring our bypass path couldn't be caught by our own listeners took several iterations — the bypass marker attribute was the solution.

Supporting Outlook alongside Gmail without duplicating everything. We wanted to share the analysis engine, modal, and settings layer across both platforms while keeping DOM-specific code isolated. The final architecture has two entry points and two interceptor modules, but a single shared analysis and UI layer underneath.

LanguageTool false positives. Comma-at-end-of-line in bulleted lists consistently triggered punctuation warnings. Suppressing them required reading raw character offsets from the API response and scanning a window around each flagged position for newline characters — a fiddlier problem than it first appeared.

Keeping the UI feeling fast. A modal that says "loading…" for five seconds would feel broken. The skeleton loader and async result-swap pattern made the extension feel responsive even on slow connections, because the countdown and buttons are interactive from the first frame.

Built With

Share this project:

Updates

Submission history