Claude Skills Guide

AI Reply Generator Chrome Extension for Gmail: Build Your Own

Email responses consume significant time for developers and power users managing high-volume inboxes. Building a custom AI reply generator as a Chrome extension gives you full control over how responses are generated, styled, and integrated into your Gmail workflow. This guide walks through the architecture, key APIs, and implementation details you’ll need to create a production-ready extension.

Extension Architecture Overview

A Chrome extension for Gmail consists of three primary components: a background service worker for API communication, a content script that injects UI elements into Gmail, and a popup or side panel for user configuration. The AI reply generation happens server-side or through an API call from the background worker, while the content script handles DOM manipulation to insert generated responses.

┌─────────────────┐     ┌──────────────────┐     ┌─────────────┐
│  Gmail UI       │ ←── │ Content Script  │ ←── │ Background  │
│  (injected UI)  │     │  (DOM access)    │     │  Worker     │
└─────────────────┘     └──────────────────┘     └──────┬──────┘
                                                       │
                                                       ↓
                                               ┌─────────────┐
                                               │  AI API     │
                                               │  (OpenAI,   │
                                               │   Claude)   │
                                               └─────────────┘

Manifest V3 Setup

Chrome extensions now require Manifest V3. Your manifest.json defines permissions and extension behavior:

{
  "manifest_version": 3,
  "name": "Gmail AI Reply Generator",
  "version": "1.0",
  "description": "Generate AI-powered replies in Gmail",
  "permissions": [
    "activeTab",
    "scripting",
    "storage"
  ],
  "host_permissions": [
    "https://mail.google.com/*"
  ],
  "action": {
    "default_popup": "popup.html",
    "default_icon": "icon.png"
  }
}

The host_permissions field is critical—without https://mail.google.com/*, your extension cannot interact with Gmail’s DOM. The storage permission lets you save API keys and user preferences.

Reading Email Context

Your content script needs to extract the email content that the AI will use to generate a reply. Gmail’s DOM structure changes periodically, so using mutation observers and multiple selectors increases reliability:

// content-script.js
function getEmailContent() {
  // Try multiple selectors for compatibility
  const selectors = [
    '.a3s.aiL',           // Email body in reading pane
    '.h7',                // Alternate selector
    '[role="main"] .a3s'  // Modern Gmail
  ];

  for (const selector of selectors) {
    const element = document.querySelector(selector);
    if (element && element.textContent.trim().length > 0) {
      return element.textContent.trim();
    }
  }

  // Fallback: get from email thread view
  const allDivs = document.querySelectorAll('div');
  for (const div of allDivs) {
    if (div.className.includes('aiL') && div.className.includes('a3s')) {
      return div.textContent.trim();
    }
  }

  return null;
}

function getSubject() {
  const subjectEl = document.querySelector('h[class*="hP"]');
  return subjectEl ? subjectEl.textContent : '';
}

The subject line helps the AI generate contextually appropriate responses. Extract both the email body and subject, then combine them in your API prompt.

Injecting the Generate Button

Your extension needs to insert a button into Gmail’s compose UI. Gmail uses dynamic class names that change with each release, so robust selectors matter:

function injectGenerateButton() {
  // Check if button already exists
  if (document.getElementById('ai-reply-btn')) return;

  // Find the compose toolbar
  const toolbarSelectors = [
    '.btC',           // Compose window toolbar
    '.oG',            // Alternate toolbar
    '[role="toolbar"]' // Semantic selector
  ];

  let toolbar = null;
  for (const selector of toolbarSelectors) {
    toolbar = document.querySelector(selector);
    if (toolbar) break;
  }

  if (!toolbar) {
    console.log('Toolbar not found, retrying...');
    return;
  }

  const button = document.createElement('button');
  button.id = 'ai-reply-btn';
  button.innerHTML = '✨ AI Reply';
  button.className = 'T-I J-J5-Ji aoO v7 T-I-atl L3';
  button.style.cssText = 'background: #1a73e8; color: white; margin-right: 8px;';

  button.addEventListener('click', async () => {
    const content = getEmailContent();
    const subject = getSubject();

    if (!content) {
      alert('Could not detect email content');
      return;
    }

    button.textContent = 'Generating...';
    button.disabled = true;

    try {
      const response = await chrome.runtime.sendMessage({
        type: 'GENERATE_REPLY',
        emailContent: content,
        subject: subject
      });

      insertReply(response.reply);
    } catch (error) {
      console.error('Generation failed:', error);
      alert('Failed to generate reply');
    } finally {
      button.textContent = '✨ AI Reply';
      button.disabled = false;
    }
  });

  toolbar.insertBefore(button, toolbar.firstChild);
}

Background Worker and API Integration

The background worker handles communication with your AI provider. Never expose API keys in the content script—keep them secure in the background:

// background.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'GENERATE_REPLY') {
    generateReply(message.emailContent, message.subject)
      .then(reply => sendResponse({ reply }))
      .catch(error => sendResponse({ error: error.message }));
    return true; // Keep channel open for async response
  }
});

async function generateReply(emailContent, subject) {
  const { apiKey, model, tone } = await chrome.storage.local.get(['apiKey', 'model', 'tone']);

  const prompt = `Based on this email:
Subject: ${subject}
Body: ${emailContent}

Generate a ${tone || 'professional'} reply. Keep it concise and actionable.`;

  const response = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${apiKey}`
    },
    body: JSON.stringify({
      model: model || 'gpt-4',
      messages: [
        { role: 'system', content: 'You are an email assistant that generates helpful, concise replies.' },
        { role: 'user', content: prompt }
      ],
      max_tokens: 200
    })
  });

  const data = await response.json();
  return data.choices[0].message.content;
}

This pattern works with OpenAI, Anthropic Claude, or any compatible API. Adjust the fetch URL and request body based on your provider.

Inserting the Generated Reply

After generating the reply, your content script needs to insert it into Gmail’s compose area:

function insertReply(text) {
  // Find the compose textarea
  const textareaSelectors = [
    'div[role="textbox"][aria-label="Body"]',
    '.Am.Al.editable',
    '[aria-label="Body"]'
  ];

  let textarea = null;
  for (const selector of textareaSelectors) {
    textarea = document.querySelector(selector);
    if (textarea) break;
  }

  if (textarea) {
    textarea.focus();
    // Insert text at cursor position
    document.execCommand('insertText', false, text);
  } else {
    console.error('Could not find compose textarea');
    // Fallback: try clicking reply first
    const replyBtn = document.querySelector('[data-original-title="Reply"]');
    if (replyBtn) {
      replyBtn.click();
      setTimeout(() => insertReply(text), 500);
    }
  }
}

The execCommand approach works reliably across Gmail’s various compose modes. For rich text compose, you may need additional handling.

User Settings Storage

Let users configure their preferences through the popup:

// popup.js - Save settings
document.getElementById('save-btn').addEventListener('click', async () => {
  const settings = {
    apiKey: document.getElementById('api-key').value,
    model: document.getElementById('model').value,
    tone: document.getElementById('tone').value
  };

  await chrome.storage.local.set(settings);
  document.getElementById('status').textContent = 'Settings saved!';
});

Store settings in chrome.storage.local rather than localStorage—localStorage doesn’t persist in Chrome’s MV3 extension model the same way.

Deployment Considerations

Before publishing to the Chrome Web Store, verify your extension handles edge cases gracefully. Test with different email formats, multi-part messages, and Gmail’s various view modes. Include error handling for API failures, rate limits, and missing permissions.

Consider adding these production features:

Building your own AI reply generator gives you flexibility that commercial tools cannot match. You control the AI model, the prompt engineering, and the UI integration. For developers managing high-volume email, this automation pays dividends in time saved.


*

Built by theluckystrike — More at zovo.one*