Build Email Snooze Chrome Extension (2026)
Email snooze functionality has become essential for managing inbox overwhelm. Chrome extensions that implement email snooze scheduling allow users to temporarily remove emails from their inbox and have them reappear at a specified future time. This guide covers the implementation patterns, APIs, and practical considerations for developers building this type of extension.
How Email Snooze Extensions Work
Chrome extensions for email snooze scheduling typically operate by interacting with email provider APIs. The extension intercepts email messages, stores the snooze metadata on a backend or locally, and then uses scheduled triggers to move or relabel those emails back to the inbox at the designated time.
The core architecture involves three main components:
- Content Script - Runs in the context of the email provider’s web interface, identifying emails and adding snooze UI elements
- Background Service Worker - Handles scheduling logic and API calls
- Storage Layer - Maintains snooze state using chrome.storage or a backend database
Architecture Decision: Local vs. Backend Storage
Before writing a single line of code, decide where your snooze state lives. This choice affects reliability, privacy, and the complexity of your extension.
| Approach | Pros | Cons |
|---|---|---|
chrome.storage.local |
No backend needed, works offline, simple | Snooze lost if user clears extension data, no cross-device sync |
chrome.storage.sync |
Cross-device sync via Google account | 100KB total quota, 8KB per item limit |
| Backend database | Full control, cross-device, recoverable | Requires auth, server cost, GDPR surface area |
| Provider labels (Gmail) | Snooze survives browser uninstall | Slower, requires API scope, pollutes label list |
For a personal productivity tool, chrome.storage.local with a fallback to provider labels is often the right balance. For a commercial product, a lightweight backend with OAuth gives users the reliability they expect.
Manifest V3 Implementation
Modern Chrome extensions must use Manifest V3, which introduces several changes from V2. Here’s a basic manifest structure for an email snooze extension:
{
"manifest_version": 3,
"name": "Email Snooze Scheduler",
"version": "1.0",
"permissions": [
"storage",
"alarms",
"activeTab",
"scripting"
],
"host_permissions": [
"https://mail.google.com/*",
"https://outlook.office.com/*"
],
"background": {
"service_worker": "background.js"
},
"content_scripts": [{
"matches": [
"https://mail.google.com/*",
"https://outlook.office.com/*"
],
"js": ["content.js"]
}]
}
The key Manifest V3 constraint to understand is that background pages are gone. you have a service worker instead. Service workers are ephemeral. They spin up to handle an event and then terminate. This means you cannot use global variables to hold state between alarm firings. Everything that needs to survive must go into chrome.storage.
Core Implementation Patterns
Storing Snooze Data
The chrome.storage API provides persistent storage that works across browser sessions. For email snooze extensions, you’ll need to store the email ID, original label, and scheduled return time.
// content.js - Snooze an email
function snoozeEmail(emailId, snoozeUntil) {
const snoozeEntry = {
emailId: emailId,
originalLabels: ['INBOX'], // Capture current labels
snoozeUntil: snoozeUntil,
provider: 'gmail' // or 'outlook', etc.
};
chrome.storage.local.get(['snoozedEmails'], (result) => {
const snoozedEmails = result.snoozedEmails || [];
snoozedEmails.push(snoozeEntry);
chrome.storage.local.set({ snoozedEmails });
});
// Notify background script to schedule the alarm
chrome.runtime.sendMessage({
action: 'scheduleSnooze',
emailId: emailId,
snoozeUntil: snoozeUntil
});
}
One subtlety: chrome.storage.local.get is asynchronous, and the callback pattern above can cause race conditions if multiple snooze actions fire close together. Prefer the Promise-based API with async/await for cleaner sequencing:
// Safer async version
async function snoozeEmail(emailId, snoozeUntil) {
const snoozeEntry = {
emailId,
originalLabels: ['INBOX'],
snoozeUntil,
provider: 'gmail',
snoozedAt: Date.now()
};
const result = await chrome.storage.local.get(['snoozedEmails']);
const snoozedEmails = result.snoozedEmails || [];
// Deduplicate: remove any existing snooze for this email before adding
const filtered = snoozedEmails.filter(e => e.emailId !== emailId);
filtered.push(snoozeEntry);
await chrome.storage.local.set({ snoozedEmails: filtered });
chrome.runtime.sendMessage({
action: 'scheduleSnooze',
emailId,
snoozeUntil
});
}
Scheduling with Chrome Alarms
Chrome’s alarms API allows you to schedule future events reliably, even when the extension isn’t actively running. The alarms API is the correct tool here. do not use setTimeout in a service worker, because the worker may terminate before the timeout fires.
// background.js
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name.startsWith('snooze-')) {
const emailId = alarm.name.replace('snooze-', '');
processSnoozeReturn(emailId);
}
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'scheduleSnooze') {
const delay = message.snoozeUntil - Date.now();
if (delay > 0) {
chrome.alarms.create(`snooze-${message.emailId}`, {
delayInMinutes: Math.ceil(delay / 60000),
periodInMinutes: null
});
}
}
});
async function processSnoozeReturn(emailId) {
// Retrieve stored email metadata
const result = await chrome.storage.local.get(['snoozedEmails']);
const snoozedEmails = result.snoozedEmails || [];
const entry = snoozedEmails.find(e => e.emailId === emailId);
if (entry) {
// Use provider API to move email back to inbox
await moveEmailToInbox(entry);
// Clean up storage
const updated = snoozedEmails.filter(e => e.emailId !== emailId);
await chrome.storage.local.set({ snoozedEmails: updated });
}
}
One important limitation: Chrome alarms have a minimum granularity of approximately one minute. If you need sub-minute precision (unusual for email snooze), you cannot achieve it with the alarms API alone. For typical snooze use cases. later today, tomorrow morning. one-minute granularity is perfectly acceptable.
Handling Browser Restarts
When Chrome restarts, existing alarms survive (they are persisted by the browser). However, if your background service worker starts fresh after a restart, it needs to re-register its alarm listener. Because listeners are registered at service worker startup, the chrome.alarms.onAlarm.addListener call at the top of your background.js handles this automatically.
What does not survive is any in-memory state. Always reload from chrome.storage when the service worker activates:
// background.js - startup check for missed snoozes
chrome.runtime.onStartup.addListener(async () => {
const result = await chrome.storage.local.get(['snoozedEmails']);
const snoozedEmails = result.snoozedEmails || [];
const now = Date.now();
for (const entry of snoozedEmails) {
if (entry.snoozeUntil <= now) {
// Alarm fired while Chrome was closed. process immediately
await processSnoozeReturn(entry.emailId);
}
}
});
This startup handler catches any snoozes that matured while the browser was closed, ensuring users see their emails when they open Chrome the next morning.
Provider API Integration
Gmail API Integration
For Gmail integration, you’ll need to use the Gmail API to modify email labels. The typical snooze flow is: remove the INBOX label (archiving the email), optionally add a custom SNOOZED label, and then on wake, add INBOX back and remove the SNOOZED label.
async function moveEmailToInbox(snoozeEntry) {
const { accessToken } = await getAccessToken();
// Remove the snooze label and restore INBOX
await fetch(
`https://gmail.googleapis.com/gmail/v1/users/me/messages/${snoozeEntry.emailId}/modify`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
removeLabelIds: ['SNOOZE_LABEL_ID'],
addLabelIds: ['INBOX', 'UNREAD']
})
}
);
}
To snooze an email, perform the inverse. remove INBOX and add your custom SNOOZED label:
async function archiveForSnooze(emailId, snoozeLabelId) {
const { accessToken } = await getAccessToken();
await fetch(
`https://gmail.googleapis.com/gmail/v1/users/me/messages/${emailId}/modify`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
removeLabelIds: ['INBOX'],
addLabelIds: [snoozeLabelId]
})
}
);
}
async function ensureSnoozeLabelExists() {
const { accessToken } = await getAccessToken();
// List existing labels and find or create the snooze label
const res = await fetch(
'https://gmail.googleapis.com/gmail/v1/users/me/labels',
{ headers: { 'Authorization': `Bearer ${accessToken}` } }
);
const data = await res.json();
const existing = data.labels.find(l => l.name === 'Snoozed');
if (existing) return existing.id;
const create = await fetch(
'https://gmail.googleapis.com/gmail/v1/users/me/labels',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Snoozed',
labelListVisibility: 'labelHide',
messageListVisibility: 'hide'
})
}
);
const created = await create.json();
return created.id;
}
Outlook API Integration
Microsoft Graph API handles Outlook.com and Office 365. The pattern mirrors Gmail but uses folder moves instead of label modifications:
async function moveOutlookEmailToInbox(snoozeEntry) {
const { accessToken } = await getAccessToken();
// Move message to inbox folder
await fetch(
`https://graph.microsoft.com/v1.0/me/messages/${snoozeEntry.emailId}/move`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
destinationId: 'inbox'
})
}
);
}
async function archiveOutlookEmailForSnooze(emailId) {
const { accessToken } = await getAccessToken();
// Move to a dedicated snooze folder
const folderId = await ensureOutlookSnoozeFolderExists(accessToken);
await fetch(
`https://graph.microsoft.com/v1.0/me/messages/${emailId}/move`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ destinationId: folderId })
}
);
}
Provider API Comparison
| Feature | Gmail API | Microsoft Graph |
|---|---|---|
| Auth flow | OAuth 2.0 (Google Identity) | OAuth 2.0 (MSAL) |
| Snooze mechanism | Label add/remove | Folder move |
| Required scope | gmail.modify |
Mail.ReadWrite |
| Rate limit | 250 quota units/user/second | 10,000 requests per 10 min |
| Batch operations | Yes (batch endpoint) | Yes (batch endpoint) |
| Webhook support | Push notifications (Cloud Pub/Sub) | Microsoft Graph subscriptions |
User Interface Considerations
The snooze UI typically appears as a dropdown or popup when users hover over or select an email. Common preset options include:
- Later Today - 6 PM or custom time
- Tomorrow Morning - 9 AM next day
- Tomorrow Evening - 6 PM next day
- This Weekend - Saturday 9 AM
- Next Week - Monday 9 AM
- Custom - User-specified date and time
Implement custom time selection using a date-time picker:
function showCustomSnoozeDialog() {
const dialog = document.createElement('div');
dialog.innerHTML = `
<div class="snooze-dialog">
<h3>Schedule Snooze</h3>
<input type="datetime-local" id="snooze-datetime">
<button id="confirm-snooze">Snooze</button>
<button id="cancel-snooze">Cancel</button>
</div>
`;
document.body.appendChild(dialog);
document.getElementById('confirm-snooze').addEventListener('click', () => {
const datetime = document.getElementById('snooze-datetime').value;
const snoozeUntil = new Date(datetime).getTime();
snoozeEmail(currentEmailId, snoozeUntil);
dialog.remove();
});
document.getElementById('cancel-snooze').addEventListener('click', () => {
dialog.remove();
});
}
Pre-populate the datetime input with a reasonable default to reduce friction:
function getDefaultSnoozeTime() {
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(9, 0, 0, 0);
// Format as YYYY-MM-DDTHH:MM for datetime-local input
const pad = n => String(n).padStart(2, '0');
return `${tomorrow.getFullYear()}-${pad(tomorrow.getMonth()+1)}-${pad(tomorrow.getDate())}T09:00`;
}
Injecting UI into Gmail
Gmail’s DOM changes frequently, which is the main fragility point for content scripts that inject UI. Target stable data attributes rather than CSS class names, which Gmail minifies and rotates:
// Prefer data attributes and role selectors over class names
const emailRows = document.querySelectorAll('[data-legacy-message-id]');
emailRows.forEach(row => {
if (!row.querySelector('.snooze-btn')) {
const btn = document.createElement('button');
btn.className = 'snooze-btn';
btn.textContent = 'Snooze';
btn.addEventListener('click', (e) => {
e.stopPropagation();
const emailId = row.dataset.legacyMessageId;
showSnoozeMenu(emailId, btn);
});
row.appendChild(btn);
}
});
Use a MutationObserver to handle Gmail’s virtual rendering, which adds and removes rows dynamically as you scroll:
const observer = new MutationObserver(() => {
injectSnoozeButtons();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
Security and Privacy
Email snooze extensions handle sensitive data, so implement these security practices:
- Minimal Permissions - Request only the scopes needed for email access
- Secure Token Storage - Use chrome.storage.session for access tokens when possible
- Content Security Policy - Restrict script sources in your manifest
- OAuth 2.0 Flow - Never store user passwords directly
{
"oauth2": {
"client_id": "YOUR_CLIENT_ID",
"scopes": [
"https://www.googleapis.com/auth/gmail.modify",
"https://www.googleapis.com/auth/gmail.readonly"
]
}
}
Use chrome.storage.session for access tokens. session storage clears when the browser closes, limiting the window of exposure if a machine is stolen:
// Store token in session storage (cleared on browser close)
async function cacheAccessToken(token, expiresIn) {
await chrome.storage.session.set({
accessToken: token,
tokenExpiry: Date.now() + (expiresIn * 1000)
});
}
async function getAccessToken() {
const result = await chrome.storage.session.get(['accessToken', 'tokenExpiry']);
if (result.accessToken && result.tokenExpiry > Date.now()) {
return { accessToken: result.accessToken };
}
// Token missing or expired. fetch a new one
return await refreshAccessToken();
}
Also add a Content Security Policy to your manifest to prevent injected scripts from loading external resources:
{
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
}
}
Testing Your Snooze Extension
Unit Testing Alarm Logic
Test the alarm scheduling logic in isolation using Jest and a mock Chrome API:
// Mock chrome.alarms for unit tests
global.chrome = {
alarms: {
create: jest.fn(),
onAlarm: { addListener: jest.fn() }
},
storage: {
local: {
get: jest.fn(),
set: jest.fn()
}
},
runtime: {
sendMessage: jest.fn(),
onMessage: { addListener: jest.fn() }
}
};
test('scheduleSnooze creates alarm with correct delay', () => {
const emailId = 'abc123';
const snoozeUntil = Date.now() + 3600000; // 1 hour from now
scheduleSnoozeAlarm(emailId, snoozeUntil);
expect(chrome.alarms.create).toHaveBeenCalledWith(
`snooze-${emailId}`,
expect.objectContaining({ delayInMinutes: 60 })
);
});
End-to-End Testing Considerations
For end-to-end tests, use Puppeteer or Playwright with the --load-extension flag to load your unpacked extension. Test the full snooze and wake cycle against a real Gmail sandbox account with test emails you control.
Building a Custom Solution
For developers building their own snooze functionality, start with the fundamental patterns shown here and expand based on your specific requirements. Consider these enhancements as your extension matures:
Snooze categories. Let users tag snoozes as “follow-up”, “waiting”, or “read later”. Surface these tags in a dedicated sidebar panel so users can see all snoozed emails at a glance without waiting for them to reappear.
Smart snooze suggestions. Analyze email content for date mentions and suggest snooze times automatically. An email saying “let me know by Friday” could trigger a Thursday afternoon suggestion.
Notification before wake. Use the Web Notifications API to alert users five minutes before a snoozed email returns, giving them context before it appears in their inbox.
Bulk snooze. Let users select multiple emails and snooze them to the same time in a single action. This is especially useful for mailing list digests or newsletters.
// Bulk snooze example
async function bulkSnooze(emailIds, snoozeUntil) {
for (const emailId of emailIds) {
await snoozeEmail(emailId, snoozeUntil);
}
}
The key is maintaining reliable scheduling even when the browser is closed, which requires combining chrome.alarms with a startup handler that catches missed firings. Pair that with a lightweight OAuth flow and the provider API patterns shown here, and you have a production-grade snooze extension that handles the full lifecycle from snooze to wake without data loss.
Try it: Estimate your monthly spend with our Cost Calculator.
Related Reading
- AI Email Writer Chrome Extension: A Developer’s Guide
- AI Screen Reader Chrome Extension: A Complete Guide for Developers
- Best Cookie Manager Chrome Extensions for Developers in 2026
- Meeting Scheduler Chrome Extension Guide (2026)
- Flash Sale Notification Chrome Extension Guide (2026)
- Chrome Extension HTML Email P — Honest Review 2026
- Noise Cancellation Chrome Extension Guide (2026)
- Pinterest Pin Scheduler Chrome Extension Guide (2026)
- Chrome Managed Profiles: Work and Personal Browsing
- Speed Up Chrome Low Ram — Developer Guide
- Chrome Extension Retrospective Board: Agile Tools
Built by theluckystrike. More at zovo.one
Configure permissions → Build your settings with our Permission Configurator.