diff --git a/ai/email-classifier/Cards.gs b/ai/email-classifier/Cards.gs new file mode 100644 index 000000000..55ffe0ea4 --- /dev/null +++ b/ai/email-classifier/Cards.gs @@ -0,0 +1,182 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Triggered when the add-on is opened from the Gmail homepage. + * + * @param {!Object} e - The event object. + * @returns {!Card} - The homepage card. + */ +function onHomepageTrigger(e) { + return buildHomepageCard(); +} + +/** + * Builds the main card displayed on the Gmail homepage. + * + * @returns {!Card} - The homepage card. + */ +function buildHomepageCard() { + // Create a new card builder + const cardBuilder = CardService.newCardBuilder(); + + // Create a card header + const cardHeader = CardService.newCardHeader(); + cardHeader.setImageUrl('https://fonts.gstatic.com/s/i/googlematerialicons/label_important/v20/googblue-24dp/1x/gm_label_important_googblue_24dp.png'); + cardHeader.setImageStyle(CardService.ImageStyle.CIRCLE); + cardHeader.setTitle("Email Classifier"); + + // Add the header to the card + cardBuilder.setHeader(cardHeader); + + // Create a card section + const cardSection = CardService.newCardSection(); + + // Create buttons for generating sample emails and analyzing sentiment + const buttonSet = CardService.newButtonSet(); + + // Create "Classify emails" button + const classifyButton = createFilledButton({ + text: 'Classify emails', + functionName: 'main', + color: '#007bff', + icon: 'new_label' + }); + buttonSet.addButton(classifyButton); + + // Create "Create Labels" button + const createLabelsButtton = createFilledButton({ + text: 'Create labels', + functionName: 'createLabels', + color: '#34A853', + icon: 'add' + }); + + // Create "Remove Labels" button + const removeLabelsButtton = createFilledButton({ + text: 'Remove labels', + functionName: 'removeLabels', + color: '#FF0000', + icon: 'delete' + }); + + if (labelsCreated()) { + buttonSet.addButton(removeLabelsButtton); + } else { + buttonSet.addButton(createLabelsButtton); + } + + // Add the button set to the section + cardSection.addWidget(buttonSet); + + // Add the section to the card + cardBuilder.addSection(cardSection); + + // Build and return the card + return cardBuilder.build(); +} + +/** + * Creates a filled text button with the specified text, function, and color. + * + * @param {{text: string, functionName: string, color: string, icon: string}} options + * - text: The text to display on the button. + * - functionName: The name of the function to call when the button is clicked. + * - color: The background color of the button. + * - icon: The material icon to display on the button. + * @returns {!TextButton} - The created text button. + */ +function createFilledButton({text, functionName, color, icon}) { + // Create a new text button + const textButton = CardService.newTextButton(); + + // Set the button text + textButton.setText(text); + + // Set the action to perform when the button is clicked + const action = CardService.newAction(); + action.setFunctionName(functionName); + action.setLoadIndicator(CardService.LoadIndicator.SPINNER); + textButton.setOnClickAction(action); + + // Set the button style to filled + textButton.setTextButtonStyle(CardService.TextButtonStyle.FILLED); + + // Set the background color + textButton.setBackgroundColor(color); + + textButton.setMaterialIcon(CardService.newMaterialIcon().setName(icon)); + + return textButton; +} + +/** + * Creates a notification response with the specified text. + * + * @param {string} notificationText - The text to display in the notification. + * @returns {!ActionResponse} - The created action response. + */ +function buildNotificationResponse(notificationText) { + // Create a new notification + const notification = CardService.newNotification(); + notification.setText(notificationText); + + // Create a new action response builder + const actionResponseBuilder = CardService.newActionResponseBuilder(); + + // Set the notification for the action response + actionResponseBuilder.setNotification(notification); + + // Build and return the action response + return actionResponseBuilder.build(); +} + +/** + * Creates a card to display the spreadsheet link. + * + * @param {string} spreadsheetUrl - The URL of the spreadsheet. + * @returns {!ActionResponse} - The created action response. + */ +function showSpreadsheetLink(spreadsheetUrl) { + const updatedCardBuilder = CardService.newCardBuilder(); + + updatedCardBuilder.setHeader(CardService.newCardHeader().setTitle('Sheet generated!')); + + const updatedSection = CardService.newCardSection() + .addWidget(CardService.newTextParagraph() + .setText('Click to open the sheet:') + ) + .addWidget(CardService.newTextButton() + .setText('Open Sheet') + .setOpenLink(CardService.newOpenLink() + .setUrl(spreadsheetUrl) + .setOpenAs(CardService.OpenAs.FULL_SCREEN) // Opens in a new browser tab/window + .setOnClose(CardService.OnClose.NOTHING) // Does nothing when the tab is closed + ) + ) + .addWidget(CardService.newTextButton() // Optional: Add a button to go back or refresh + .setText('Go Back') + .setOnClickAction(CardService.newAction() + .setFunctionName('onHomepageTrigger')) // Go back to the initial state + ); + + updatedCardBuilder.addSection(updatedSection); + const newNavigation = CardService.newNavigation().updateCard(updatedCardBuilder.build()); + + return CardService.newActionResponseBuilder() + .setNavigation(newNavigation) // This updates the current card in the UI + .build(); +} diff --git a/ai/email-classifier/ClassifyEmail.gs b/ai/email-classifier/ClassifyEmail.gs new file mode 100644 index 000000000..cda44a7ce --- /dev/null +++ b/ai/email-classifier/ClassifyEmail.gs @@ -0,0 +1,133 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Constructs the prompt for classifying an email. + * + * @param {string} subject The subject of the email. + * @param {string} body The body of the email. + * @returns {string} The prompt for classifying an email. + */ +const classifyEmailPrompt = (subject, body) => ` +Objective: You are an AI assistant tasked with classifying email threads. Analyze the entire email thread provided below and determine the single most appropriate classification label. Your response must conform to the provided schema. + +**Classification Labels & Descriptions:** + +* **needs-response**: The sender is explicitly or implicitly expecting a **direct, communicative reply** from me (${ME}) to answer a question, acknowledge receipt of information, confirm understanding, or continue a conversation. **Prioritize this label if the core expectation is purely a written or verbal communication back to the sender.** +* **action-required**: The email thread requires me (${ME}) to perform a **distinct task, make a formal decision, provide a review leading to approval/rejection, or initiate a process that results in a demonstrable change or outcome.** This label is for actions *beyond* just sending a reply, such as completing a document, setting up a meeting, approving a request, delegating a task, or performing a delegated duty. +* **for-your-info**: The email thread's primary purpose is to convey information, updates, or announcements. No immediate action or direct reply is expected or required from me (${ME}); the main purpose is for me to be informed and aware. This includes both routine 'FYI' updates and critical announcements where my role is to comprehend, not act or respond. + +**Evaluation Criteria - Consider the following:** + +* **Sender's Intent & My Role:** What does the sender want me (${ME}) to do, say, or know? +* **Direct Requests:** Are there explicit questions or calls to action addressed to me (${ME})? +* **Distinguishing Action vs. Response:** + * If the email primarily asks for a *verbal or written communication* (e.g., answering a specific question, providing feedback, confirming receipt, giving thoughts, and is directly addressed to me (${ME})), it's likely \`needs-response\`. + * If the email requires me to *perform a specific task or make a formal decision that goes beyond simply communicating* (e.g., completing a document, scheduling, approving a request, delegating, implementing a change), it's likely \`action-required\`. +* **Urgency/Deadlines:** Are there time-sensitive elements mentioned? +* **Last Message Focus:** Give slightly more weight to the content of the most recent messages in the thread. +* **Keywords:** + * Look for terms like "answer," "reply to," "your thoughts on," "confirm," "acknowledge" for \`needs-response\`. + * Look for terms like "complete," "approve," "review and approve," "sign," "process," "set up," "delegate" for \`action-required\`. + * Look for terms like "FYI," "update," "announcement," "read," "info" for \`for-information\`. +* **Overall Significance:** Is the topic critical or routine, influencing the *type* of information being conveyed? + +**Input:** Email message content +Subject: ${subject} + +--- Email Thread Messages --- +${body} +--- End of Email Thread --- + +**Output:** Return the single best classification and a brief justification. +Format: JSON object with '[Classification]', and '[Reason]' +`.trim(); + +/** + * Classifies an email based on its subject and messages. + * + * @param {string} subject The subject of the email. + * @param {!Array} messages An array of Gmail messages. + * @returns {!Object} The classification object. + */ +function classifyEmail(subject, messages) { + const body = []; + for (let i = 0; i < messages.length; i++) { + const message = messages[i]; + body.push(`Message ${i + 1}:`); + body.push(`From: ${message.getFrom()}`); + body.push(`To:${message.getTo()}`); + body.push('Body:'); + body.push(message.getPlainBody()); + body.push('---'); + } + + // Prepare the request payload + const payload = { + contents: [ + { + role: "user", + parts: [ + { + text: classifyEmailPrompt(subject, body.join('\n')) + } + ] + } + ], + generationConfig: { + temperature: 0, + topK: 1, + topP: 0.1, + seed: 37, + maxOutputTokens: 1024, + responseMimeType: "application/json", + // Expected response format for simpler parsing. + responseSchema: { + type: "object", + properties: { + classification: { + type: "string", + enum: Object.keys(classificationLabels), + }, + reason: { + type: 'string' + } + } + } + } + }; + + // Prepare the request options + const options = { + method: 'POST', + headers: { + 'Authorization': `Bearer ${ScriptApp.getOAuthToken()}` + }, + contentType: 'application/json', + muteHttpExceptions: true, // Set to true to inspect the error response + payload: JSON.stringify(payload) + }; + + // Make the API request + const response = UrlFetchApp.fetch(API_URL, options); + + // Parse the response. There are two levels of JSON responses to parse. + const parsedResponse = JSON.parse(response.getContentText()); + const text = parsedResponse.candidates[0].content.parts[0].text; + const classification = JSON.parse(text); + return classification; +} + diff --git a/ai/email-classifier/Code.gs b/ai/email-classifier/Code.gs new file mode 100644 index 000000000..c802e8192 --- /dev/null +++ b/ai/email-classifier/Code.gs @@ -0,0 +1,67 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Main function to process emails, classify them, and update a spreadsheet. + * This function searches for unread emails in the inbox from the last 7 days, + * classifies them based on their subject and content, adds labels to the emails, + * creates draft responses for emails that need a response, and logs the + * classification results in a spreadsheet. + * @return {string} The URL of the spreadsheet. + */ +function main() { + // Calculate the date 7 days ago + const today = new Date(); + const sevenDaysAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000); + + // Create a Sheet + const headers = ['Subject', 'Classification', 'Reason']; + const spreadsheet = createSheetWithHeaders(headers); + + // Format the date for the Gmail search query (YYYY/MM/DD) + // Using Utilities.formatDate ensures correct formatting based on script + // timezone + const formattedDate = Utilities.formatDate( + sevenDaysAgo, Session.getScriptTimeZone(), 'yyyy/MM/dd'); + + // Construct the search query + const query = `is:unread after:${formattedDate} in:inbox`; + console.log('Searching for emails with query: ' + query); + + // Search for threads matching the query + // Note: GmailApp.search() returns threads where *at least one* message + // matches + const threads = GmailApp.search(query); + createLabels(); + + for (const thread of threads) { + const messages = thread.getMessages(); + const subject = thread.getFirstMessageSubject(); + const {classification, reason} = classifyEmail(subject, messages); + console.log(`Classification: ${classification}, Reason: ${reason}`); + + thread.addLabel(classificationLabels[classification].gmailLabel); + + if (classification === 'needs-response') { + const draft = draftEmail(subject, messages); + thread.createDraftReplyAll(null, {htmlBody: draft}); + } + + addDataToSheet(spreadsheet, hyperlink(thread), classification, reason); + } + + return showSpreadsheetLink(spreadsheet.getUrl()); +} diff --git a/ai/email-classifier/Constants.gs b/ai/email-classifier/Constants.gs new file mode 100644 index 000000000..2908b83ba --- /dev/null +++ b/ai/email-classifier/Constants.gs @@ -0,0 +1,23 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const PROJECT_ID = ''; +const LOCATION = 'us-central1'; +const API_ENDPOINT = `${LOCATION}-aiplatform.googleapis.com`; +const MODEL = 'gemini-2.5-pro-preview-05-06'; +const GENERATE_CONTENT_API = 'generateContent'; +const API_URL = `https://${API_ENDPOINT}/v1/projects/${PROJECT_ID}/locations/${LOCATION}/publishers/google/models/${MODEL}:${GENERATE_CONTENT_API}`; +const ME = ''; diff --git a/ai/email-classifier/DraftEmail.gs b/ai/email-classifier/DraftEmail.gs new file mode 100644 index 000000000..f016386ba --- /dev/null +++ b/ai/email-classifier/DraftEmail.gs @@ -0,0 +1,141 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Constructs a prompt for drafting an email. + * + * @param {string} subject The subject of the email thread. + * @param {string} body The body of the email thread. + * @returns {string} The prompt string. + */ +const draftEmailPrompt = (subject, body) => ` +You are an AI assistant. Based on the following email thread: + +Subject: ${subject} + +--- Email Thread Messages --- +${body} +--- End of Email Thread --- + +Task: Considering all messages in this thread: +- Help me ${ME} draft a polite and professional reply that addresses the key points from the most recent message(s) in HTML +- Do NOT include subject of the email + +Draft Criteria: Consider the following: +* Explicit Questions: Are there direct questions posed to me ${ME}, especially in the most recent messages? +* Calls to Action: Are there clear instructions or requests for the me ${ME}, to *do* something? +* Urgency/Deadlines: Does the thread mention deadlines or urgent requests? +* Sender's Intent: What does the sender seem to want? +* My Role: What am I (${ME}) being asked to do or know? +* Keywords: Look for terms like "please," "urgent," "FYI," "question," "task," "review," "approve," "respond," "deadline." +* Last Message Focus: Give slightly more weight to the most recent messages. +* Overall Significance: Is the topic critical or routine? + +Output: Return the draft message in HTML format. +Format: HTML + +Example format: + + +
+This is an HTML email sent from Google Apps Script.
+ + +`.trim(); + +/** + * Drafts an email based on the given subject and messages. + * + * @param {string} subject The subject of the email thread. + * @param {!Array} messages An array of Gmail messages. + * @returns {string|null} The drafted email in HTML format or null if not found. + */ +function draftEmail(subject, messages) { + const body = []; + for (let i = 0; i < messages.length; i++) { + const message = messages[i]; + body.push(`Message ${i + 1}:`); + body.push(`From: ${message.getFrom()}`); + body.push(`To:${message.getTo()}`); + body.push('Body:'); + body.push(message.getPlainBody()); + body.push('---'); + } + + // Prepare the request payload + const payload = { + contents: [ + { + role: "user", + parts: [ + { + text: draftEmailPrompt(subject, body.join('\n')) + } + ] + } + ], + generationConfig: { + temperature: 0, + topK: 1, + topP: 0.1, + seed: 37, + maxOutputTokens: 1024, + responseMimeType: 'text/plain' + } + }; + + // Prepare the request options + const options = { + method: 'POST', + headers: { + 'Authorization': `Bearer ${ScriptApp.getOAuthToken()}` + }, + contentType: 'application/json', + muteHttpExceptions: true, // Set to true to inspect the error response + payload: JSON.stringify(payload) + }; + + // Make the API request + const response = UrlFetchApp.fetch(API_URL, options); + + // Parse the response. There are two levels of JSON responses to parse. + const parsedResponse = JSON.parse(response.getContentText()); + const draft = parsedResponse.candidates[0].content.parts[0].text; + return extractHtmlContent(draft); +} + +/** + * Extracts HTML content from a string. + * + * @param {string} textString The string to extract HTML content from. + * @returns {string|null} The HTML content or null if not found. + */ +function extractHtmlContent(textString) { + // The regex pattern: + // ````html` (literal start marker) + // `(.*?)` (capturing group for any character, non-greedily, including newlines) + // ` ``` ` (literal end marker) + // `s` flag makes '.' match any character including newlines. + const match = textString.match(/```html(.*?)```/s); + if (match && match[1]) { + return match[1]; // Return the content of the first capturing group + } + return null; // Or an empty string, depending on desired behavior if not found +} diff --git a/ai/email-classifier/Labels.gs b/ai/email-classifier/Labels.gs new file mode 100644 index 000000000..4f6505ad3 --- /dev/null +++ b/ai/email-classifier/Labels.gs @@ -0,0 +1,108 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const classificationLabels = { + "action-required": { + "name": "🚨 Action Required", + "textColor": '#ffffff', + "backgroundColor": "#1c4587" + }, + "needs-response": { + "name": "↪️ Needs Response", + "textColor": '#ffffff', + "backgroundColor": "#16a765" + }, + "for-your-info": { + "name": "ℹ️ For Your Info", + "textColor": '#000000', + "backgroundColor": "#fad165" + }, +}; + +/** + * Creates Gmail labels based on the classification labels defined in the `classificationLabels` object. + * If a label already exists, it updates the color. Otherwise, it creates a new label. + * After creating or updating labels, it logs a message to the console and returns the homepage card. + * @returns {!CardService.Card} The homepage card. + */ +function createLabels() { + for (const labelName in classificationLabels) { + const classificationLabel = classificationLabels[labelName]; + const { name, textColor, backgroundColor } = classificationLabel; + let gmailLabel = GmailApp.getUserLabelByName(name); + + if (!gmailLabel) { + gmailLabel = GmailApp.createLabel(name); + Gmail.Users.Labels.update({ + name: name, + color: { + textColor: textColor, + backgroundColor: backgroundColor + } + }, 'me', fetchLabelId(name)); + } + + classificationLabel.gmailLabel = gmailLabel; + } + + console.log('Labels created.'); + return buildHomepageCard(); +} + +/** + * Checks if all classification labels exist in Gmail. + * @returns {boolean} True if all labels exist, false otherwise. + */ +function labelsCreated() { + for (const labelName in classificationLabels) { + const { name } = classificationLabels[labelName]; + let gmailLabel = GmailApp.getUserLabelByName(name); + + if (!gmailLabel) { + return false; + } + } + + return true; +} + +/** + * Fetches the ID of a Gmail label by its name. + * @param {string} name The name of the label. + * @returns {string} The ID of the label. + */ +function fetchLabelId(name) { + return Gmail.Users.Labels.list('me').labels.find(_ => _.name === name).id; +} + +/** + * Removes all classification labels from Gmail. + * After removing labels, it logs a message to the console and returns the homepage card. + * @returns {!CardService.Card} The homepage card. + */ +function removeLabels() { + for (const labelName in classificationLabels) { + const classificationLabel = classificationLabels[labelName]; + let gmailLabel = GmailApp.getUserLabelByName(classificationLabel.name); + + if (gmailLabel) { + gmailLabel.deleteLabel(); + delete classificationLabel.gmailLabel; + } + } + console.log('Labels removed.'); + return buildHomepageCard(); +} diff --git a/ai/email-classifier/README.md b/ai/email-classifier/README.md new file mode 100644 index 000000000..050dd6c43 --- /dev/null +++ b/ai/email-classifier/README.md @@ -0,0 +1,144 @@ +# Email Classifier + +This Apps Script project provides a Gmail add-on that classifies emails based on +their content and subject, and performs actions such as adding labels, creating +draft responses, and logging results in a Google Sheet. It leverages the Gemini +API for natural language processing. + +## Features + +* **Email Classification:** Classifies unread emails in your inbox into three + categories: + * `needs-response`: Emails requiring a direct reply. + * `action-required`: Emails requiring a specific task or decision. + * `for-your-info`: Emails for information only, no action needed. +* **Labeling:** Adds Gmail labels to emails based on their classification. +* **Draft Responses:** Generates draft email responses for emails classified + as `needs-response`. +* **Spreadsheet Logging:** Logs email details, classification, and reason to a + Google Sheet. +* **User-Friendly Interface:** Provides a Gmail add-on with buttons for + classification, label creation, and removal. + +## Setup + +### 1. Enable Google APIs + +* Go to the [Google Cloud Console](https://console.cloud.google.com/). +* Create or select a project. +* **Gemini API:** + * [Enable the Gemini API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com) +* **Gmail API:** + * [Enable the Gmail API](https://console.cloud.google.com/flows/enableapi?apiid=gmail.googleapis.com) +* **Sheets API:** + * [Enable the Sheets API](https://console.cloud.google.com/flows/enableapi?apiid=sheets.googleapis.com) + +### 2. Apps Script Project + +1. **Create a New Project:** + * Go to [script.google.com](https://script.google.com). + * Create a new project. +1. **Enable `appsscript.json` Manifest:** + * Go to **Project Settings**. + * Check the **Show "appsscript.json" manifest file in editor** option. +1. **Associate with Google Cloud Project:** + * In your Apps Script project, go to **Project Settings**. + * Under **Google Cloud Platform (GCP) Project**, click **Change project**. + * Enter your Google Cloud Project number and click **Set project**. +1. **Copy Code:** + * Copy the code from each `.gs` file in this directory into the + corresponding file in your Apps Script project. +1. **Update `Constants.gs`:** + * Replace the placeholder values in `Constants.gs`: + * `PROJECT_ID`: Your Google Cloud Project ID. + * `ME`: Your name. +1. **Update `appsscript.gson`:** + * Ensure the `appsscript.gson` file is configured correctly. + +### 3. Configure OAuth Consent Screen + +Google Workspace add-ons require a consent screen configuration. Configuring +your add-on's OAuth consent screen defines what Google displays to users. + +1. **Go to Google Cloud Console:** + * Navigate to the [Google Auth Platform - Branding page](https://console.cloud.google.com/auth/branding). +1. **App Information:** + * **App name:** Enter a name for your add-on (e.g., "Email Classifier"). + * **User support email:** Select your email address. + * **Developer contact information:** Enter your email address. + * Click **Next**. +1. **Audience:** + * Select **Internal**. + * Click **Next**. +1. **Contact Information:** + * Select your email address. + * Click **Next**. +1. **Finish:** + * Check **I agree to the [Google API Services: User Data Policy](https://developers.google.com/terms/api-services-user-data-policy)**. + * Click **Continue**. +1. **Create:** + * Click **Create**. + +### 4. Deploy the Add-on + +1. **Deploy:** + * Click "Deploy" > "Test deployments". + * Select "Gmail add-on". + * Click "Install" to install the add-on for your account. + +## How to Run + +1. **Open Gmail:** + * Open Gmail in your browser. +1. **Open the Add-on:** + * The "Email Classifier" add-on should appear in the right sidebar. +1. **Classify Emails:** + * Click the "Classify emails" button. + * The add-on will process unread emails from the last 7 days. +1. **View Results:** + * A link to the generated Google Sheet will be displayed. + * Open the sheet to view the classification results. +1. **Create/Remove Labels:** + * Use the "Create labels" or "Remove labels" buttons to manage Gmail labels. + +## Code Overview + +* **`Cards.gs`:** + * Defines the UI for the Gmail add-on, including buttons and actions. +* **`ClassifyEmail.gs`:** + * Constructs prompts for the Gemini API. + * Sends email content to the Gemini API for classification. + * Parses the API response. +* **`Code.gs`:** + * Main function to search, classify, label, and log emails. +* **`Constants.gs`:** + * Stores project-specific constants (e.g., API URL, project ID, email). +* **`DraftEmail.gs`:** + * Constructs prompts for the Gemini API to generate draft responses. + * Sends email content to the Gemini API for draft generation. + * Parses the API response. +* **`Labels.gs`:** + * Creates, updates, and removes Gmail labels. +* **`Sheet.gs`:** + * Creates and updates Google Sheets for logging. +* **`appsscript.gson`:** + * Configuration file for the Apps Script project. + +## Important Notes + +* **Gemini API Usage:** This project relies on the Gemini API for natural + language processing. Make sure you have the API enabled and have sufficient + quota. +* **OAuth Scopes:** The `appsscript.json` file includes the necessary OAuth + scopes for Gmail, Sheets, and the Gemini API. +* **Error Handling:** The code includes basic error handling, but you may need + to add more robust error handling for production use. +* **Rate Limits:** Be mindful of API rate limits, especially when processing + large numbers of emails. +* **Security:** Ensure that you are handling user data securely. + +## Disclaimer + +This code is provided as-is, without any warranty. Use at your own risk. + +Feel free to modify and adapt this code to your specific needs. diff --git a/ai/email-classifier/Sheet.gs b/ai/email-classifier/Sheet.gs new file mode 100644 index 000000000..b665d757a --- /dev/null +++ b/ai/email-classifier/Sheet.gs @@ -0,0 +1,84 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Creates a spreadsheet with the given headers. + * @param {!Array