{
"manifest_version": 3,
"name": "Gmail AI Assistant",
"version": "0.5.0",
"description": "Intelligently sorts and prioritizes your emails.",
"permissions": [
"storage",
"activeTab",
"scripting",
"identity"
],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": [
"https://mail.google.com/*"
],
"js": [
"content.js"
],
"css": [
"styles.css"
]
}
],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "images/icon16.png",
"48": "images/icon48.png",
"128": "images/icon128.png"
}
},
"icons": {
"16": "images/icon16.png",
"48": "images/icon48.png",
"128": "images/icon128.png"
},
"oauth2": {
"client_id": "YOUR_CLIENT_ID.apps.googleusercontent.com",
"scopes": [
"https://www.googleapis.com/auth/gmail.modify"
]
}
}
```javascript
// background.js
console.log("Gmail AI Assistant background script loaded.");
// Set default settings when the extension is installed.
chrome.runtime.onInstalled.addListener(() => {
chrome.storage.sync.set({
senderCounts: {},
settings: {
enableRepetitive: true,
enablePromotional: true,
enablePersonal: true
}
});
});
/**
* Handles messages sent from the content script to perform actions.
*/
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'createFilter') {
getAuthToken(token => {
createFilter(token, request.data.senderEmail);
});
} else if (request.action === 'markPriority') {
getAuthToken(token => {
applyPriorityLabel(token, request.data.threadId);
});
}
return true; // Indicates that the response will be sent asynchronously.
});
/**
* Prompts the user for authentication and retrieves an OAuth 2.0 token.
* @param {function(string)} callback - The function to call with the retrieved token.
*/
function getAuthToken(callback) {
chrome.identity.getAuthToken({ interactive: true }, (token) => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError.message);
return;
}
callback(token);
});
}
/**
* Creates a new label named "Priority" if it doesn't already exist.
* @param {string} token - The OAuth 2.0 token.
* @returns {Promise<string>} A promise that resolves with the ID of the "Priority" label.
*/
async function getOrCreatePriorityLabelId(token) {
// First, list existing labels to see if "Priority" already exists.
const listResponse = await fetch('[https://gmail.googleapis.com/gmail/v1/users/me/labels](https://gmail.googleapis.com/gmail/v1/users/me/labels)', {
headers: { 'Authorization': `Bearer ${token}` }
});
const listData = await listResponse.json();
const priorityLabel = listData.labels.find(label => label.name === 'Priority');
if (priorityLabel) {
return priorityLabel.id; // Return existing label ID
}
// If it doesn't exist, create it.
const createResponse = await fetch('[https://gmail.googleapis.com/gmail/v1/users/me/labels](https://gmail.googleapis.com/gmail/v1/users/me/labels)', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Priority',
labelListVisibility: 'labelShow',
messageListVisibility: 'show'
})
});
const createData = await createResponse.json();
console.log('Created "Priority" label:', createData);
return createData.id;
}
/**
* Applies the "Priority" label to a specific email thread.
* @param {string} token - The OAuth 2.0 token.
* @param {string} threadId - The ID of the thread to label.
*/
async function applyPriorityLabel(token, threadId) {
const labelId = await getOrCreatePriorityLabelId(token);
const response = await fetch(`https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}/modify`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
addLabelIds: [labelId]
})
});
const data = await response.json();
console.log('Applied Priority label to thread:', data);
}
/**
* Creates a Gmail filter to archive emails from a specific sender.
* @param {string} token - The OAuth 2.0 token.
* @param {string} senderEmail - The email address of the sender to filter.
*/
async function createFilter(token, senderEmail) {
const filter = {
criteria: { from: senderEmail },
action: { addLabelIds: ['TRASH'] } // Archives and moves to trash
};
const response = await fetch('[https://gmail.googleapis.com/gmail/v1/users/me/settings/filters](https://gmail.googleapis.com/gmail/v1/users/me/settings/filters)', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(filter)
});
const data = await response.json();
console.log('Filter created:', data);
}
```javascript
// content.js
console.log("Gmail AI Assistant content script loaded and running.");
const PROMOTION_KEYWORDS = [
'unsubscribe', 'view in browser', 'no longer wish', 'special offer',
'limited time', 'percent off', 'free shipping', 'view this email'
];
/**
* Scans all visible emails for various patterns and injects contextual prompts based on user settings.
*/
function processVisibleEmails() {
const emailRows = document.querySelectorAll('tr.zA');
if (emailRows.length === 0) return;
// Get user settings and sender counts from storage
chrome.storage.sync.get(['senderCounts', 'settings'], (result) => {
let counts = result.senderCounts || {};
const settings = result.settings || {
enableRepetitive: true,
enablePromotional: true,
enablePersonal: true
};
let updated = false;
emailRows.forEach(row => {
if (row.dataset.promptInjected) return;
const senderEl = row.querySelector('.yW span[email]');
if (senderEl) {
const senderEmail = senderEl.getAttribute('email');
const senderName = senderEl.getAttribute('name');
// --- 1. Personal Conversation Logic ---
const threadCountEl = row.querySelector('.bA4 span');
if (settings.enablePersonal && threadCountEl && parseInt(threadCountEl.innerText) > 2) {
const threadId = row.closest('tr').getAttribute('data-thread-id');
if (threadId) {
injectPersonalConversationPrompt(row, senderName, threadId);
row.dataset.promptInjected = 'true';
return;
}
}
// --- 2. Repetitive Sender Logic ---
if (!counts[senderEmail]) {
counts[senderEmail] = 0;
}
counts[senderEmail] += 1;
updated = true;
const threshold = 4;
if (settings.enableRepetitive && counts[senderEmail] >= threshold) {
injectRepetitiveEmailPrompt(row, senderName, senderEmail, counts[senderEmail]);
row.dataset.promptInjected = 'true';
return;
}
// --- 3. Promotional Email Logic ---
const snippetEl = row.querySelector('.y2');
if (settings.enablePromotional && snippetEl) {
const snippetText = snippetEl.innerText.toLowerCase();
if (PROMOTION_KEYWORDS.some(keyword => snippetText.includes(keyword))) {
injectPromotionPrompt(row, senderName);
row.dataset.promptInjected = 'true';
return;
}
}
}
});
if (updated) {
chrome.storage.sync.set({ senderCounts: counts });
}
});
}
/**
* Injects a prompt for personal conversations.
* @param {HTMLElement} targetRow The email row.
* @param {string} senderName The sender's name.
* @param {string} threadId The ID of the email thread.
*/
function injectPersonalConversationPrompt(targetRow, senderName, threadId) {
const promptContainer = createPromptContainer();
const promptContent = document.createElement('div');
promptContent.className = 'gmail-ai-prompt personal';
promptContent.innerHTML = `
<div class="prompt-icon">💬</div>
<div class="prompt-text">This looks like an important conversation with <strong>${senderName}</strong>. Mark as priority?</div>
<div class="prompt-actions">
<button class="prompt-button yes" data-action="mark-priority">Mark Priority</button>
<button class="prompt-button no" data-action="dismiss">Dismiss</button>
</div>
`;
promptContainer.firstChild.appendChild(promptContent);
targetRow.parentNode.insertBefore(promptContainer, targetRow);
promptContainer.querySelector('[data-action="dismiss"]').addEventListener('click', (e) => { e.stopPropagation(); promptContainer.remove(); });
promptContainer.querySelector('[data-action="mark-priority"]').addEventListener('click', (e) => {
e.stopPropagation();
chrome.runtime.sendMessage({ action: 'markPriority', data: { threadId } });
promptContainer.remove();
});
}
/**
* Injects a prompt for likely promotions.
*/
function injectPromotionPrompt(targetRow, senderName) {
const promptContainer = createPromptContainer();
const promptContent = document.createElement('div');
promptContent.className = 'gmail-ai-prompt promotion';
promptContent.innerHTML = `
<div class="prompt-icon">🛍️</div>
<div class="prompt-text">This looks like a promotional email from <strong>${senderName}</strong>.</div>
<div class="prompt-actions">
<button class="prompt-button yes" data-action="unsubscribe">Unsubscribe</button>
<button class="prompt-button no" data-action="dismiss">Dismiss</button>
</div>
`;
promptContainer.firstChild.appendChild(promptContent);
targetRow.parentNode.insertBefore(promptContainer, targetRow);
promptContainer.querySelector('[data-action="dismiss"]').addEventListener('click', (e) => { e.stopPropagation(); promptContainer.remove(); });
promptContainer.querySelector('[data-action="unsubscribe"]').addEventListener('click', (e) => {
e.stopPropagation();
alert('Automated unsubscribe coming in a future update!');
promptContainer.remove();
});
}
/**
* Injects the prompt for repetitive emails.
*/
function injectRepetitiveEmailPrompt(targetRow, senderName, senderEmail, count) {
const promptContainer = createPromptContainer();
const promptContent = document.createElement('div');
promptContent.className = 'gmail-ai-prompt repetitive';
promptContent.innerHTML = `
<div class="prompt-icon">💡</div>
<div class="prompt-text">You've received <strong>${count}</strong> emails from <strong>${senderName}</strong>. Would you like to create a filter?</div>
<div class="prompt-actions">
<button class="prompt-button yes" data-action="create-filter">Create Filter</button>
<button class="prompt-button no" data-action="dismiss">Dismiss</button>
</div>
`;
promptContainer.firstChild.appendChild(promptContent);
targetRow.parentNode.insertBefore(promptContainer, targetRow);
promptContainer.querySelector('[data-action="dismiss"]').addEventListener('click', (e) => { e.stopPropagation(); promptContainer.remove(); });
promptContainer.querySelector('[data-action="create-filter"]').addEventListener('click', (e) => {
e.stopPropagation();
chrome.runtime.sendMessage({ action: 'createFilter', data: { senderEmail } });
promptContainer.remove();
});
}
function createPromptContainer() {
const container = document.createElement('tr');
container.className = 'gmail-ai-prompt-container';
const cell = document.createElement('td');
cell.colSpan = "100%";
container.appendChild(cell);
return container;
}
function observeGmail() {
const observer = new MutationObserver((mutationsList) => {
for(const mutation of mutationsList) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
setTimeout(processVisibleEmails, 500);
return;
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
console.log("Mutation observer is now watching the Gmail interface.");
setTimeout(processVisibleEmails, 1000);
}
const initInterval = setInterval(() => {
if (document.querySelector('.Cp')) {
clearInterval(initInterval);
observeGmail();
}
}, 500);
```css
/* styles.css */
/* --- In-Page Prompt Styles --- */
.gmail-ai-prompt-container {
border: none !important;
box-shadow: none !important;
}
.gmail-ai-prompt {
border-radius: 8px;
padding: 12px 16px;
margin: 6px 16px 6px 72px;
font-family: 'Google Sans', Roboto, Arial, sans-serif;
font-size: 14px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.gmail-ai-prompt.repetitive {
background-color: #f0f4f9;
border: 1px solid #d4e3fb;
}
.gmail-ai-prompt.promotion {
background-color: #fce8e6;
border: 1px solid #f9d9d6;
}
.gmail-ai-prompt.promotion .prompt-text strong {
color: #c5221f;
}
.gmail-ai-prompt.personal {
background-color: #e6f4ea;
border: 1px solid #cce8d4;
}
.gmail-ai-prompt.personal .prompt-text strong {
color: #1e8e3e;
}
.prompt-icon {
font-size: 20px;
}
.prompt-text {
flex-grow: 1;
color: #3c4043;
}
.prompt-text strong {
font-weight: 500;
color: #1a73e8;
}
.prompt-actions {
display: flex;
gap: 8px;
}
.prompt-button {
background-color: transparent;
border: 1px solid #dadce0;
border-radius: 4px;
padding: 8px 16px;
font-family: inherit;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s, box-shadow 0.2s;
}
.prompt-button.yes {
background-color: #1a73e8;
color: white;
border-color: #1a73e8;
}
.prompt-button.yes:hover {
background-color: #287ae6;
box-shadow: 0 1px 2px 0 rgba(60,64,67,0.3), 0 1px 3px 1px rgba(60,64,67,0.15);
}
.gmail-ai-prompt.promotion .prompt-button.yes {
background-color: #d93025;
border-color: #d93025;
}
.gmail-ai-prompt.promotion .prompt-button.yes:hover {
background-color: #e04a40;
}
.gmail-ai-prompt.personal .prompt-button.yes {
background-color: #1e8e3e;
border-color: #1e8e3e;
}
.gmail-ai-prompt.personal .prompt-button.yes:hover {
background-color: #25a24c;
}
.prompt-button.no {
color: #5f6368;
}
.prompt-button.no:hover {
background-color: #f1f3f4;
}
/* --- Popup Styles --- */
body {
font-family: 'Google Sans', Roboto, Arial, sans-serif;
width: 280px;
padding: 0;
background-color: #f8f9fa;
}
.popup-header {
background-color: #4285f4;
color: white;
padding: 16px;
text-align: center;
}
.popup-header h1 {
font-size: 18px;
margin: 0;
font-weight: 500;
}
.settings-list {
padding: 8px 0;
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #e0e0e0;
}
.setting-item:last-child {
border-bottom: none;
}
.setting-label {
font-size: 14px;
color: #3c4043;
}
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 34px;
height: 20px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
border-radius: 20px;
}
.slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #4285f4;
}
input:focus + .slider {
box-shadow: 0 0 1px #4285f4;
}
input:checked + .slider:before {
-webkit-transform: translateX(14px);
-ms-transform: translateX(14px);
transform: translateX(14px);
}
```html
<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
<title>Gmail AI Assistant Settings</title>
<link href="styles.css" rel="stylesheet">
</head>
<body>
<div class="popup-header">
<h1>AI Assistant Settings</h1>
</div>
<div class="settings-list">
<div class="setting-item">
<span class="setting-label">Repetitive Email Prompts</span>
<label class="switch">
<input type="checkbox" id="enableRepetitive">
<span class="slider"></span>
</label>
</div>
<div class="setting-item">
<span class="setting-label">Promotional Email Prompts</span>
<label class="switch">
<input type="checkbox" id="enablePromotional">
<span class="slider"></span>
</label>
</div>
<div class="setting-item">
<span class="setting-label">Personal Conversation Prompts</span>
<label class="switch">
<input type="checkbox" id="enablePersonal">
<span class="slider"></span>
</label>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
```javascript
// popup.js
document.addEventListener('DOMContentLoaded', () => {
const repetitiveToggle = document.getElementById('enableRepetitive');
const promotionalToggle = document.getElementById('enablePromotional');
const personalToggle = document.getElementById('enablePersonal');
// Load current settings and update the UI
chrome.storage.sync.get('settings', (data) => {
if (data.settings) {
repetitiveToggle.checked = data.settings.enableRepetitive;
promotionalToggle.checked = data.settings.enablePromotional;
personalToggle.checked = data.settings.enablePersonal;
}
});
// Save settings when a toggle is changed
repetitiveToggle.addEventListener('change', (event) => {
updateSettings({ enableRepetitive: event.target.checked });
});
promotionalToggle.addEventListener('change', (event) => {
updateSettings({ enablePromotional: event.target.checked });
});
personalToggle.addEventListener('change', (event) => {
updateSettings({ enablePersonal: event.target.checked });
});
});
/**
* Updates the settings in chrome.storage.
* @param {object} newSettings - An object with the settings to update.
*/
function updateSettings(newSettings) {
chrome.storage.sync.get('settings', (data) => {
const updatedSettings = { ...data.settings, ...newSettings };
chrome.storage.sync.set({ settings: updatedSettings });
});
}
```
// Create dummy icon files named icon16.png, icon48.png, and icon128.png
// and place them in an 'images' folder. For now, they can be any simple image.
Comments