{
"manifest_version": 3,
"name": "Gmail AI Assistant",
"version": "0.4.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.");
/**
* 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'
];
function processVisibleEmails() {
const emailRows = document.querySelectorAll('tr.zA');
if (emailRows.length === 0) return;
chrome.storage.sync.get(['senderCounts'], (result) => {
let counts = result.senderCounts || {};
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 (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 (counts[senderEmail] >= threshold) {
injectRepetitiveEmailPrompt(row, senderName, senderEmail, counts[senderEmail]);
row.dataset.promptInjected = 'true';
return;
}
// --- 3. Promotional Email Logic ---
const snippetEl = row.querySelector('.y2');
if (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);
}
function injectInitialUI() {
const composeButton = document.querySelector('.T-I.T-I-KE.L3');
if (composeButton && !document.getElementById('gmail-ai-assistant-marker')) {
const ourUI = document.createElement('div');
ourUI.id = 'gmail-ai-assistant-marker';
ourUI.textContent = 'AI Assistant Active';
ourUI.style.padding = '5px 10px';
ourUI.style.backgroundColor = '#4285F4';
ourUI.style.color = 'white';
ourUI.style.marginLeft = '10px';
ourUI.style.borderRadius = '4px';
composeButton.parentElement.appendChild(ourUI);
}
}
const initInterval = setInterval(() => {
if (document.querySelector('.Cp')) {
clearInterval(initInterval);
injectInitialUI();
observeGmail();
}
}, 500);
```css
/* styles.css */
/* This style ensures the container for our prompt doesn't have unwanted borders or lines from Gmail's default table styling. */
.gmail-ai-prompt-container {
border: none !important;
box-shadow: none !important;
}
/* The main container for our injected prompt. */
.gmail-ai-prompt {
border-radius: 8px;
padding: 12px 16px;
margin: 6px 16px 6px 72px; /* Aligns with Gmail's content, leaving space for checkboxes */
font-family: 'Google Sans', Roboto, Arial, sans-serif;
font-size: 14px;
display: flex;
align-items: center;
gap: 16px; /* Provides spacing between the icon, text, and buttons */
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
/* --- Repetitive Email Prompt --- */
.gmail-ai-prompt.repetitive {
background-color: #f0f4f9; /* A light, friendly blue */
border: 1px solid #d4e3fb;
}
/* --- Promotional Email Prompt --- */
.gmail-ai-prompt.promotion {
background-color: #fce8e6; /* A light, cautionary red/pink */
border: 1px solid #f9d9d6;
}
.gmail-ai-prompt.promotion .prompt-text strong {
color: #c5221f; /* A darker red for highlighted text */
}
/* --- Personal Conversation Prompt --- */
.gmail-ai-prompt.personal {
background-color: #e6f4ea; /* A light, positive green */
border: 1px solid #cce8d4;
}
.gmail-ai-prompt.personal .prompt-text strong {
color: #1e8e3e; /* A darker green for highlighted text */
}
/* Styling for the lightbulb icon */
.prompt-icon {
font-size: 20px;
}
/* The text portion of the prompt */
.prompt-text {
flex-grow: 1; /* Allows the text to take up the remaining space */
color: #3c4043;
}
.prompt-text strong {
font-weight: 500;
color: #1a73e8; /* Default highlight color */
}
/* Container for the action buttons */
.prompt-actions {
display: flex;
gap: 8px;
}
/* Base styling for the prompt buttons */
.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;
}
/* Specific styling for the 'Yes' or confirmation button */
.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);
}
/* --- Promotion-specific button styling --- */
.gmail-ai-prompt.promotion .prompt-button.yes {
background-color: #d93025;
border-color: #d93025;
}
.gmail-ai-prompt.promotion .prompt-button.yes:hover {
background-color: #e04a40;
}
/* --- Personal-specific button styling --- */
.gmail-ai-prompt.personal .prompt-button.yes {
background-color: #1e8e3e;
border-color: #1e8e3e;
}
.gmail-ai-prompt.personal .prompt-button.yes:hover {
background-color: #25a24c;
}
/* Specific styling for the 'No' or dismiss button */
.prompt-button.no {
color: #5f6368;
}
.prompt-button.no:hover {
background-color: #f1f3f4;
}
```html
<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
<title>Gmail AI Assistant</title>
<style>
body {
font-family: 'Google Sans', Roboto, Arial, sans-serif;
width: 250px;
padding: 10px;
text-align: center;
}
h1 {
font-size: 16px;
color: #202124;
}
p {
font-size: 14px;
color: #5f6368;
}
</style>
</head>
<body>
<h1>Gmail AI Assistant</h1>
<p>The assistant is active and analyzing your inbox.</p>
<p>Future settings and controls will appear here.</p>
</body>
</html>
```
// 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