├── README.md ├── background.js ├── content.js ├── manifest.json ├── media └── hero.png ├── options.html ├── options.js ├── popup.html └── popup.js /README.md: -------------------------------------------------------------------------------- 1 | # GmailSponge 2 | 3 | A tool for cleaning up your inbox. Very hacky and experimental, use at your own risk. Made With Claude™ 4 | 5 | ![](media/hero.png) 6 | 7 | ## Getting Started 8 | 9 | 1. Clone the repo 10 | 2. Go to chrome://extensions and activate developer mode 11 | 3. Load the folder as an unpacked extension 12 | 4. The icon (a G) should now appear when you're on a Gmail tab. Right click it and open options. 13 | 5. Enter your Anthropic API key. You have one, right anon? You should probably also switch the model to Sonnet. 14 | 6. Click "Save" to save those settings. 15 | 7. Enter a prompt. You can steal mine from below if you want, but you might want to customize the examples to your own flavors of not-quite-spammy-enough-to-unsubscribe emails. Give it a name and save. 16 | 8. Go back to Gmail and click the icon. Click "Run prompt" on your prompt. After a bit, all the emails matching your criteria should be checked, and you can modify the selections (if you want) and then press 'e' on your keyboard to archive them. 17 | 9. Rinse and repeat until inbox zero (or inbox 22, or whatever). 18 | 19 | ## My prompt 20 | 21 | ``` 22 | You are a mail archival assistant. You will receive a numbered list of emails, and should return (in a JSON code block) a list of numbers corresponding to emails that should be archived. 23 | 24 | Emails that should be archived: 25 | - Notifications, alerts, Github overages, invites, receipts, deliveries, parcel pending, or other emails that state their purpose in the subject and can be discarded simply after reading the subject. 26 | - Corporate mailing list content and similar. 27 | - USPS daily digests and similar. 28 | - "PDF from " emails with a paper title--this is weird spam I get from Academia.edu 29 | 30 | Emails that should NOT be archived: 31 | - Personal emails, from real people instead of corporations (don't be fooled by corporations using "real" names--"Melanie at Warp" is a corporation, not a person) 32 | ``` 33 | 34 | Whatever prompt you use will be prepended to the prompt in `background.js`, so it's worth checking there to see what it already specifies (some <scratchpad> CoT and JSON formatting.) 35 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | console.log('Background script loaded'); 2 | 3 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 4 | console.log('Message received in background:', request); 5 | if (request.action === "callLLM") { 6 | callLLM(request.prompt, request.emailList, request.settings) 7 | .then(response => { 8 | console.log('LLM call successful:', response); 9 | sendResponse({ status: 'success', data: response }); 10 | }) 11 | .catch(error => { 12 | console.error('LLM call failed:', error); 13 | sendResponse({ status: 'error', message: error.message, details: error.details }); 14 | }); 15 | return true; // Will respond asynchronously 16 | } 17 | }); 18 | 19 | async function callLLM(prompt, emailList, settings) { 20 | const response = await fetch(settings.endpoint, { 21 | method: 'POST', 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | 'x-api-key': settings.apiKey, 25 | 'anthropic-version': '2023-06-01', 26 | 'anthropic-dangerous-direct-browser-access': "true", 27 | }, 28 | body: JSON.stringify({ 29 | model: settings.model, 30 | messages: [ 31 | { role: "user", content: `${prompt} 32 | 33 | Here are the emails: 34 | ${emailList} 35 | 36 | Respond with a JSON array of numbers in a ${"```json"} code block, corresponding to the emails that should be checked. Before responding, open a and, for each email in the list, briefly think with a few words about whether it should be archived. Only then open the JSON code block and respond with the numbers of emails to archive, as a JSON list.` } 37 | ], 38 | max_tokens: 4000, 39 | }), 40 | }); 41 | 42 | if (!response.ok) { 43 | const errorText = await response.text(); 44 | console.error('API error response:', errorText); 45 | throw new Error(`HTTP error! status: ${response.status}, details: ${errorText}`); 46 | } 47 | 48 | const data = await response.json(); 49 | console.log(data); 50 | return data.content[0].text.split("```json")[1].split("```")[0]; 51 | } 52 | -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | console.log('Content script loaded'); 2 | 3 | chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => { 4 | console.log('Message received in content script:', request); 5 | if (request.action === "runPrompt") { 6 | processEmails(request.prompt) 7 | .then(() => { 8 | console.log('Emails processed successfully'); 9 | sendResponse({status: 'success'}); 10 | }) 11 | .catch((error) => { 12 | console.error('Error in processEmails:', error); 13 | sendResponse({status: 'error', message: error.toString(), details: error.details}); 14 | }); 15 | return true; // will send a response asynchronously 16 | } 17 | }); 18 | 19 | async function processEmails(prompt) { 20 | console.log('Processing emails with prompt:', prompt); 21 | const emailRows = document.querySelectorAll('tr[role="row"]'); 22 | let emailList = []; 23 | 24 | emailRows.forEach((row, index) => { 25 | const subjectElement = row.querySelector('span[data-thread-id]'); 26 | const subject = subjectElement ? subjectElement.textContent.trim() : 'No subject'; 27 | 28 | const senderElement = row.querySelector('span[email]'); 29 | const sender = senderElement ? senderElement.getAttribute('email') : 'Unknown sender'; 30 | 31 | emailList.push(`${index + 1}. From: ${sender} - Subject: ${subject}`); 32 | }); 33 | 34 | console.log('Extracted emails:', emailList); 35 | 36 | console.log('Fetching settings...'); 37 | const settings = await new Promise((resolve) => chrome.storage.sync.get(['model', 'endpoint', 'apiKey'], resolve)); 38 | console.log('Settings fetched:', settings); 39 | 40 | console.log('Sending message to background script...'); 41 | const response = await chrome.runtime.sendMessage({ 42 | action: "callLLM", 43 | prompt: prompt, 44 | emailList: emailList.join('\n'), 45 | settings: settings 46 | }); 47 | console.log('Response from background script:', response); 48 | 49 | if (response.status === 'error') { 50 | throw new Error(`${response.message}\nDetails: ${response.details || 'No additional details'}`); 51 | } 52 | 53 | const emailsToCheck = JSON.parse(response.data); 54 | 55 | emailsToCheck.forEach((index) => { 56 | const checkbox = emailRows[index - 1]?.querySelector('div[role="checkbox"]'); 57 | if (checkbox) { 58 | checkbox.click(); 59 | } 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "GmailSponge", 4 | "version": "1.0", 5 | "description": "An AI-powered assistant to help you select emails in Gmail", 6 | "permissions": [ 7 | "storage", 8 | "activeTab" 9 | ], 10 | "host_permissions": [ 11 | "https://mail.google.com/*", 12 | "https://api.anthropic.com/*" 13 | ], 14 | "action": { 15 | "default_popup": "popup.html" 16 | }, 17 | "options_page": "options.html", 18 | "background": { 19 | "service_worker": "background.js", 20 | "type": "module" 21 | }, 22 | "content_scripts": [ 23 | { 24 | "matches": [ 25 | "https://mail.google.com/*" 26 | ], 27 | "js": [ 28 | "content.js" 29 | ] 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /media/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgel/gmailsponge/f63ed526dc56dae0c910fd70da836edceb27b45f/media/hero.png -------------------------------------------------------------------------------- /options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GmailSponge Settings 5 | 12 | 13 | 14 |

GmailSponge Settings

15 | 16 |

API Settings

17 |
18 | 19 | 24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 | 32 |
33 | 34 | 35 | 36 |

Prompts

37 |

Add New Prompt

38 |
39 | 40 | 41 | 42 |
43 | 44 |

Existing Prompts

45 |
46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | let prompts = []; 2 | 3 | function saveSettings() { 4 | const model = document.getElementById('model').value; 5 | const endpoint = document.getElementById('endpoint').value; 6 | const apiKey = document.getElementById('apiKey').value; 7 | 8 | chrome.storage.sync.set({ 9 | prompts: prompts, 10 | model: model, 11 | endpoint: endpoint, 12 | apiKey: apiKey 13 | }, () => { 14 | console.log('Settings saved'); 15 | }); 16 | } 17 | 18 | function addPrompt(event) { 19 | event.preventDefault(); 20 | const promptName = document.getElementById('promptName').value; 21 | const promptText = document.getElementById('promptText').value; 22 | 23 | if (promptName && promptText) { 24 | prompts.push({ name: promptName, text: promptText }); 25 | renderPrompts(); 26 | saveSettings(); 27 | document.getElementById('promptForm').reset(); 28 | } 29 | } 30 | 31 | function deletePrompt(index) { 32 | prompts.splice(index, 1); 33 | renderPrompts(); 34 | saveSettings(); 35 | } 36 | 37 | function renderPrompts() { 38 | const promptsContainer = document.getElementById('prompts'); 39 | promptsContainer.innerHTML = ''; 40 | 41 | prompts.forEach((prompt, index) => { 42 | const promptDiv = document.createElement('div'); 43 | promptDiv.className = 'prompt'; 44 | promptDiv.innerHTML = ` 45 |

${prompt.name}

46 |

${prompt.text}

47 | 48 | `; 49 | promptsContainer.appendChild(promptDiv); 50 | }); 51 | 52 | // Add event listeners to delete buttons 53 | document.querySelectorAll('.deletePrompt').forEach(button => { 54 | button.addEventListener('click', function() { 55 | deletePrompt(parseInt(this.getAttribute('data-index'))); 56 | }); 57 | }); 58 | } 59 | 60 | document.addEventListener('DOMContentLoaded', () => { 61 | chrome.storage.sync.get(['prompts', 'model', 'endpoint', 'apiKey'], (result) => { 62 | prompts = result.prompts || []; 63 | renderPrompts(); 64 | 65 | if (result.model) document.getElementById('model').value = result.model; 66 | if (result.endpoint) document.getElementById('endpoint').value = result.endpoint; 67 | if (result.apiKey) document.getElementById('apiKey').value = result.apiKey; 68 | }); 69 | 70 | document.getElementById('promptForm').addEventListener('submit', addPrompt); 71 | document.getElementById('save').addEventListener('click', saveSettings); 72 | }); -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GmailSponge 5 | 9 | 10 | 11 |

GmailSponge

12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | const promptSelect = document.getElementById('promptSelect'); 3 | const runButton = document.getElementById('runPrompt'); 4 | const statusDiv = document.getElementById('status'); 5 | 6 | chrome.storage.sync.get('prompts', (result) => { 7 | const prompts = result.prompts || []; 8 | prompts.forEach((prompt) => { 9 | const option = document.createElement('option'); 10 | option.value = prompt.text; 11 | option.textContent = prompt.name; 12 | promptSelect.appendChild(option); 13 | }); 14 | }); 15 | 16 | runButton.addEventListener('click', () => { 17 | const selectedPrompt = promptSelect.value; 18 | if (selectedPrompt) { 19 | statusDiv.textContent = 'Running prompt...'; 20 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 21 | const currentTab = tabs[0]; 22 | if (currentTab.url.includes('mail.google.com')) { 23 | chrome.tabs.sendMessage(currentTab.id, {action: "runPrompt", prompt: selectedPrompt}, (response) => { 24 | if (chrome.runtime.lastError) { 25 | console.error('Error:', chrome.runtime.lastError.message); 26 | statusDiv.textContent = 'Error: ' + chrome.runtime.lastError.message; 27 | } else if (response && response.status === 'success') { 28 | statusDiv.textContent = 'Prompt executed. Check Gmail for results.'; 29 | } else { 30 | statusDiv.textContent = 'Error: Unexpected response from content script.'; 31 | } 32 | }); 33 | } else { 34 | statusDiv.textContent = 'Error: Not on Gmail. Please open Gmail and try again.'; 35 | } 36 | }); 37 | } else { 38 | statusDiv.textContent = 'Please select a prompt.'; 39 | } 40 | }); 41 | }); --------------------------------------------------------------------------------