andy pai's tils

Today I came across an awesome script by @FabioFleitas called AI Email Filtering.js which I used to declutter my Gmail inbox.

I modified the original script to better meet my initial requirements. So how does the script work?

This script starts by analyzing my 10 most recent emails. It checks if the sender is new and if the email has already been labeled. If unlabeled, it uses OpenAI's GPT API to figure out if the email is spam, marketing, or recurring. Based on the response, it either labels and archives the email as "AI: Likely Spam" or marks it as "AI: Reviewed" to prevent unnecessary API calls and costs the next time the inbox is checked.

Before setting up the script, I created a new API key in OpenAI's dashboard and saved my Organization ID for later since it's required to make API calls.

Step 1: Create Gmail Labels

Click on "Create a new label" on the left of your Gmail client

gmail ai create new label

and then...

Step 2: Setup the Google Apps Script

gmail ai run script

Step 3: Setup the automated triggers on Google Apps Script

gmail ai script triggers

gmail ai add trigger

That covers the setup! With the script in place, my inbox is significantly cleaner, and the best part is, it only costs a dollar or two per month to run.

Full script

/* global Logger, UrlFetchApp, GmailApp */

const OPEN_AI_KEY = 'sk-xxxxxxx'
const OPEN_AI_ORG = 'org-xxxxx'
const OPEN_AI_MODEL = 'gpt-3.5-turbo-0125'

const systemMessage =
  'You are a highly intelligent AI trained to identify if the email is sales outreach, marketing, or a recurring email like newsletters. You only respond with Yes or No'

const promptMessage = `Is this email a sales, marketing or recurring email?
# Criteria:
- Sales: Product/service offers with action calls.
- Marketing: Promotions, events, updates.
- Recurring: Newsletters, digests with curated content or updates.
Only respond with Yes or No`

const spamLabel = 'AI: Likely Spam'
const reviewedLabel = 'AI: Reviewed'

const hasLabel = (thread, labelName) =>
  thread.getLabels().some((label) => label.getName() === labelName)

const haveIEmailedThisAddress = (emailAddress) => {
  const query = `to:${emailAddress} in:sent`
  return GmailApp.search(query).length > 0
}

const createPrompt = (text) => {
  // Remove long links and HTML tags
  const content = text
    .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
    .replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
    .replace(/<a\b[^<]*(?:(?!<\/a>)<[^<]*)*<\/a>/gi, '')
    .replace(/<img\b[^<]*>/gi, '')
    .replace(/&nbsp;/gi, ' ')
    .replace(/&amp;/gi, '&')
    .replace(/&lt;/gi, '<')
    .replace(/&gt;/gi, '>')
    .replace(/<\/?[^>]+>/gi, '')
    .replace(/\s{2,}/g, ' ')
    .replace(/[\r\n]+/gm, '\n')
    .replace(/&(?:[a-z\d]+|#\d+|#x[a-f\d]+);/gi, '')
    .slice(0, 2000)
  return `Body: ${content}

${promptMessage}`
}

const classifyEmailWithChatGPT = (body) => {
  const apiEndpoint = 'https://api.openai.com/v1/chat/completions'
  const payload = {
    model: OPEN_AI_MODEL,
    messages: [
      { role: 'system', content: systemMessage },
      { role: 'user', content: createPrompt(body) },
    ],
    temperature: 0.5,
    max_tokens: 4,
    top_p: 0.5,
  }

  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    headers: {
      Authorization: `Bearer ${OPEN_AI_KEY}`,
      'OpenAI-Organization': OPEN_AI_ORG,
    },
  }

  const response = UrlFetchApp.fetch(apiEndpoint, options)
  const jsonResponse = JSON.parse(response.getContentText())

  const latestResponse = jsonResponse.choices[0].message.content.trim()
  return latestResponse.toLowerCase().includes('yes')
}

const processThread = (
  thread,
  aiEmailSpamLabel,
  processedByAIEmailFilterLabel,
) => {
  const lastMessage = thread.getMessages().pop()
  const fromAddress = lastMessage.getFrom()

  if (!haveIEmailedThisAddress(fromAddress)) {
    const emailBody = lastMessage.getPlainBody()
    const isSpamEmail = classifyEmailWithChatGPT(emailBody)
    if (isSpamEmail) {
      thread.addLabel(aiEmailSpamLabel)
      thread.moveToArchive()
    } else {
      thread.addLabel(processedByAIEmailFilterLabel)
    }
  }
}

const run = () => {
  const threads = GmailApp.getInboxThreads(0, 10)
  const aiEmailSpamLabel = GmailApp.getUserLabelByName(spamLabel)
  const processedByAIEmailFilterLabel =
    GmailApp.getUserLabelByName(reviewedLabel)

  threads.forEach((thread) => {
    if (thread.isUnread() && !hasLabel(thread, reviewedLabel)) {
      if (hasLabel(thread, spamLabel)) {
        thread.moveToArchive()
        return
      }

      processThread(thread, aiEmailSpamLabel, processedByAIEmailFilterLabel)
    }
  })
}