# Project export: Matcha: Email Client

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2024
- Tagline: Emails, reimagined as LLM-powered dashboards
- Devpost: https://devpost.com/software/matcha-email-client
- GitHub: https://github.com/Shad0wSeven/treehacks
- Team: 1 GitHub contributor(s) — Ayush Nayak (4 commits)

## Devpost submission (written by the team)

### Inspiration

The inspiration behind Matcha stemmed from our collective frustration with the overwhelming nature of email overload. We envisioned a tool that could streamline the email sorting process.

### What it does

Matcha utilizes LLMs to automatically categorize and prioritize incoming emails based on their content and context. Matcha intelligently sorts emails into relevant folders, and identifies summaries.

### How we built it

We had malvyn learn react to build the frontend, while ayush and justin worked on the backend and making a flask server to serve requests.

### Challenges we ran into

It was really hard to get the llm's working and we ended up not having enough tokens to actually do anything useful which sucked becuase it broke for the demo. also using the gmail api was hard

### Accomplishments we're proud of

in the end we were really happy to get the gmail api working, that was really cool and just working on making a good looking slick user interface

### What's next

we wanted too make it even better and actually integrate the llm's properly into everything.

## README (from the GitHub repository)

# treehacks project justin wu malvyn lai ayush nayak

*Make sure to commit everything!!!*

## Deployment via Cloudflare

Once pushed, Cloudflare will be triggered to push to Cloudflare pages. If you do not see a site being published, check for build errors, and if there are none, commit something random, like an edit to this readme (add a space somewhere) to force a re-rebuild. If that doesn't work, contact Ayush.

### Links

[client-1eh.pages.dev](https://client-1eh.pages.dev/)

<div align="center">

<img src="https://github.com/matchaclient/client/assets/19739712/ce337f61-cb63-4833-b2e4-2674964bebb2" height="200">

<img src="https://github.com/matchaclient/client/assets/19739712/8b6539b0-416f-40cc-9498-d172806f0a10" height="200">

<img src="https://github.com/matchaclient/client/assets/19739712/8ee0bd55-4d2f-4786-98a1-958318114627" height="200">

<img src="https://github.com/matchaclient/client/assets/19739712/39c4738f-9585-49a9-96b9-d6c0e9f5ecc5" height="200">

</div>


## Detected evidence (automated analysis)

Indexed codebase: 28 recognized source files, 122 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- Flask (technology) — claimed on Devpost, not found in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- React (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (31 of 31)

```
app.py
gmail.py
index.html
llm.py
README.md
replit.nix
scratch.md
src/Api.js
src/App.css
src/App.jsx
src/Components/ActionBar.jsx
src/Components/EmailView.jsx
src/Components/Frame.jsx
src/Components/Item.jsx
src/Components/Modal.jsx
src/Components/Preview.jsx
src/Components/ProjectItem.jsx
src/Components/SingleEmail.jsx
src/Components/Summary.jsx
src/Components/Testcomponent.jsx
src/Components/utilities.js
src/Dashboard.jsx
src/index.jsx
src/Login.jsx
src/Mail.jsx
src/Profile.jsx
src/tailwind.css
tailwind.config.js
tsconfig.json
view.json
vite.config.js
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- Update README.md
- Update README.md
- Update README.md
- issues

## Key source files (fetched from GitHub, selected and truncated for size)

### scratch.md

```markdown
Category 1:

1880d376dc337791
187ba8027bd29cc6
18732e45ff25487b
187ab8dc09fd81c7
18716f1d312362d3
Category 2:

18789e5a99b27da4
1872a057b28a1df0
187eb1b1f660c2d5
187c015e46ea8255
187ba6e65195b819
Category 3:

187c32f5942313b5
1873b06486b2d17b
187a42acc8fc25fd
187daabd06a4f15e
18774a48115e7be5
Category 4:

187b1681796e42a0
187f4a19052a9bda
1879763f746a4e6e
1876e2456f3f22e9
187cd58b712357fb
Category 5:

187dfc4f4e34bcdb
18724d48c0623202
1871a932c2391f23
18778f100fc092d6
187a98667f384940
Category 6:

187b8804ff82c65d
18743dbfcf11e9c5
18708eeb992f59ef
1877259946e8d4f9
187e8a5f27bad8e4
Category 7:

187343f3095a8262
18779b4e1d8e9865
187fe52ad079a927
1876d97c0daa059e
187a6d0f184f2df1
Category 8:

1878d6b94f7fec0b
187e61f51473bc3b
187b8e1b967b0109
187b4fc210e1a246
1870ed73fe86ae53
Category 9:

187ee1890a3b8e5e
187f7fa925fc1da5
187fa152b96fe2e6
187a54f5dcd71687
187f6ace83c1740b
Category 10:

187b2b70a49ef93c
187c648b6b8b6779
187ad62a8dbd5f4e
187680e166a0ed1c
187c875cb6f4e829
```

### app.py

```python
import os
import pickle
import requests
import json
import contextlib
import sys
import random
# Flask
from flask import Flask, jsonify, request, send_file
from flask_cors import CORS
# Firebase
import firebase_admin
from firebase_admin import firestore
from firebase_admin import credentials
# Gmail API utils
from googleapiclient.http import BatchHttpRequest
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow, Flow
from google.auth.transport.requests import Request
# for encoding/decoding messages in base64
from base64 import urlsafe_b64decode, urlsafe_b64encode
# for dealing with attachement MIME types
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
from email.mime.audio import MIMEAudio
from email.mime.base import MIMEBase
from mimetypes import guess_type as guess_mime_type
from chardet import detect

# external imports
from gmail import *
from llm import *

app = Flask(__name__)
CORS(app)


@app.route("/")
def hello():
  return jsonify({"message": "Welcome to the Matcha v3 (Beta) API! Now with direct GMAIL implementation!"})


@app.route("/test/<user_id>")
def testUser(user_id):
	service = gmail_authenticate(user_id) #TODO: make sure this is a valid ID once firebase comes into play.
	return jsonify({"status": "success", "message": f"hello {user_id}"})


@app.route("/getid/<user_id>/<message_id>")
def getMessage(user_id, message_id):
	service = gmail_authenticate(user_id) #TODO: make sure this is a valid ID once firebase comes into play.
	message = service.users().messages().get(userId='me', id=message_id).execute()
	with nostdout():
		decoded = read_message(service, message, download=False)
	if decoded["bodyHTML"]:
		try:
			decoded["bodyHTML"] = decoded["bodyHTML"].decode("utf-8")
		except:
			pass
	else:
		decoded["bodyHTML"] = ""
		decoded["bodyText"] = "No Body Text"
	
	return jsonify(decoded)

@app.route("/getraw/<user_id>/<message_id>")
def getRawMessage(user_id, message_id):
	service = gmail_authenticate(user_id) #TODO: make sure this is a valid ID once firebase comes into play.
	message = service.users().messages().get(userId='me', id=message_id).execute()
	return jsonify(message)


@app.route("/getattachment/<user_id>/<message_id>/<attachment_name>")
def getAttachment(user_id, message_id, attachment_name):
	service = gmail_authenticate(user_id) #TODO: make sure this is a valid ID once firebase comes into play.
	message = service.users().messages().get(userId='me', id=message_id).execute()
	with nostdout():
		decoded = read_message(service, message, download=False, attachmentData=True)
	if decoded["Attachments"]: 
		# find the attachment with the given ID
		# print("attachments found")
		for attachment in decoded["Attachments"]:
			if attachment["filename"] == attachment_name:
				try:
					# print("found attachment")
					# print(attachment["data"])
					f = open(attachment["filename"], "wb")
					f.write(attachment["data"])
					f.close()
					file_handle = open(attachment["filename"], "rb")
					os.remove(attachment["filename"])
					return send_file(path_or_file=file_handle, download_name=attachment["filename"], as_attachment=True, mimetype=attachment["mime"])
				except Exception as e:
					print(e)
					return jsonify({"message": "Error downloading attachment"}), 400			
				
	return jsonify({"message": "Attachment not found"}), 400


@app.route("/search/<user_id>/<query>")
def searchMessages(user_id, query):
	service = gmail_authenticate(user_id) #TODO: make sure this is a valid ID once firebase comes into play.
	messages = search_messages(service, query)
	decoded = batchGetMessages(messages, service)
	messageList = batchToList(decoded, service)
	return jsonify(messageList)


@app.route("/search-id/<user_id>/<query>")
def searchMessagesID(user_id, query):
	service = gmail_authenticate(user_id) #TODO: make sure this is a valid ID once firebase comes into play.
	messages = search_messages(service, query)
	return jsonify(messages)


@app.route("/getlatest/<user_id>", defaults={"number": 100}) # maybe do full db download later...
@app.route("/getlatest/<user_id>/<number>")
def getLatest(user_id, number):
	service = gmail_authenticate(user_id) #TODO: make sure this is a valid ID once firebase comes into play.
	messages = service.users().messages().list(userId='me', maxResults=number).execute()
	# print(messages)
	decoded = batchGetMessages(messages["messages"], service)
	messageList = batchToList(decoded, service)
	return jsonify(messageList)

@app.route("/getlatest/<user_id>", defaults={"number": 1000}) 
@app.route("/unlimitedlatest/<user_id>/<number>")
def getUnlimitedLatest(user_id, number):
	service = gmail_authenticate(user_id) #TODO: make sure this is a valid ID once firebase comes into play.
	result = service.users().messages().list(userId='me', maxResults=number).execute()
	messages = [ ]
	if 'messages' in result:
		messages.extend(result['messages'])
	while 'nextPageToken' in result:
		maxNum = int(number) - len(messages)
		if(len(messages) >= int(number)):
			break
		# print("loading messages. . .")
		page_token = result['nextPageToken']
		result = service.users().messages().list(userId='me', maxResults=maxNum, pageToken=page_token).execute()
		if 'messages' in result:
			messages.extend(result['messages'])
	# return jsonify({"messages": len(messages)})
	decoded = batchLargeGetMessages(messages, service)
	return jsonify(decoded)

@app.route("/getpagetoken/<user_id>/<number>")
def getPageToken(user_id, number):
	# keep going until you can get the page token for the last page 
	service = gmail_authenticate(user_id) #TODO: make sure this is a valid ID once firebase comes into play.
	result = service.users().messages().list(userId='me', maxResults=number).execute()
	pt = result['nextPageToken']
	messages = [ ]
	if 'messages' in result:
		messages.extend(result['messages'])
	while 'nextPageToken' in result:
		maxNum = int(number) - len(messages)
		if(
[truncated — 9688 more characters]
```

### src/index.jsx

```javascript
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'


ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)
```

### src/App.jsx

```javascript
import './App.css'
import React, { useState, useEffect } from "react";
import Mail from './Mail';
import Dashboard from './Dashboard';
import Login from './Login';
import Profile from './Profile';
import { BrowserRouter as Router, Routes, Route, Link }
  from 'react-router-dom';
import Hotkeys from 'react-hot-keys';
// switch this to server data
// console.log(data) 
import ActionBar from './Components/ActionBar';
import { GoogleOAuthProvider } from '@react-oauth/google';
import { onAuthStateChanged } from "firebase/auth";
import { auth } from "./firebase";


export default function App() {
  const [actionOpen, setActionOpen] = React.useState(false)
  const [loggedIn, setLoggedIn] = useState(true);
  const [uid, setUid] = useState();
  // console.log(currentEmail)
  function searchBar() {

    setActionOpen(current => !current);
  }
  useEffect(() => {
    onAuthStateChanged(auth, (user) => {
      if (user) {
        // User is signed in, see docs for a list of available properties
        // https://firebase.google.com/docs/reference/js/firebase.User
        const uid = user.uid;
        // ...
        //   console.log("uid", uid)
        setUid(user.uid);
      } else {
		setLoggedIn(false);
        // User is signed out
        // ...

      }
    });
  }, []);


  return (
    <GoogleOAuthProvider clientId="802850278396-ti1tgm6tuun8qqbl0elq1up6p44u3sh9.apps.googleusercontent.com">
      <nav className="fixed w-full h-16 backdrop-blur-lg  flex flex-wrap items-center justify-between px-2 border-gray-700 border-b mb-0">
        {loggedIn && (<div className="w-full px-4 ml-0 mx-auto flex flex-wrap items-center justify-between">
          <div className="static block justify-start  font-medium leading-relaxed text-white ">
            <a
              className=" inline-block mr-5 py-5 whitespace-nowrap hover:text-blue-500"
              href="/dash"
            >
              Dashboard
            </a>
            <a
              className="inline-block mr-5 py-5  whitespace-nowrap hover:text-blue-500"
              href="/"
            >
              Inbox
            </a>
            <a
              className="inline-block mr-5 py-5  whitespace-nowrap hover:text-blue-500"
              href="#not-done"
            >
              Draft New Message
            </a>

            {/* <a
              className="inline-block mr-5 py-5 whitespace-nowrap hover:text-blue-500"
              href="/gmail">
              Gmail Test
            </a> */}





          </div>
		  <div className="float-right mr-5 text-white font-semibold">

			<a
              className="mr-5 whitespace-nowrap hover:text-blue-500"
              href="/profile"
            >
              {uid}
            </a>
		  </div>
		  

        </div>)} {!loggedIn && (
			<div className="w-full px-4 ml-0 mx-auto flex flex-wrap items-center justify-between">
			<div className="static block justify-start  font-medium leading-relaxed text-white ">
				<a
              className="inline-block mr-5 py-5  whitespace-nowrap hover:text-blue-500"
              href="/"
            >
             Discover Matcha
            </a>
			<a
              className="inline-block mr-5 py-5  whitespace-nowrap hover:text-blue-500"
              href="/"
            >
             Contact Support
            </a>
			<a
              className="inline-block mr-5 py-5  whitespace-nowrap hover:text-blue-500"
              href="/"
            >
             Join Waitlist
            </a>
			</div></div>
		)}
        
      </nav>
      <Hotkeys keyName='ctrl+k,cmd+k' onKeyDown={searchBar}>
        {actionOpen && (
          <ActionBar />
        )}

      </Hotkeys>
      <Router>
        <Routes>
          <Route path="/" element={<Mail />} />
          <Route path="/dash" element={<Dashboard />} />
		  <Route path="/login" element={<Login />} />
		  <Route path="/profile" element={<Profile />} />
        </Routes>
      </Router>
    </GoogleOAuthProvider>
  )
}

```

### vite.config.js

```javascript
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    host: '0.0.0.0',
  }
})

```

### tailwind.config.js

```javascript
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./**/*.{js,jsx,ts,tsx,html}",
  ],
  theme: {
    screens: {
      sm: '480px',
      md: '768px',
      lg: '976px',
      xl: '1440px',
    },

    fontFamily: {

    },
    extend: {
      spacing: {
        '128': '32rem',
        '144': '36rem',
      },
      borderRadius: {
        '4xl': '2rem',
      }
    }
  },
  plugins: [],
}


```

### index.html

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Matcha</title>
<script src="https://cdn.tailwindcss.com"></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/index.jsx"></script>
    <!-- <link rel="stylesheet" href="/dist/output.css"/> -->
		
    <!--
    This script places a badge on your repl's full-browser view back to your repl's cover
    page. Try various colors for the theme: dark, light, red, orange, yellow, lime, green,
    teal, blue, blurple, magenta, pink!
    -->
    <!-- <script src="https://replit.com/public/js/replit-badge-v2.js" theme="dark" position="bottom-right"></script> -->
  </body>
</html>

```

### llm.py

```python
import json
import requests

# hf api
GPT_2_API_URL = "https://api-inference.huggingface.co/models/gpt2"
GPT_2_L_API_URL = "https://api-inference.huggingface.co/models/gpt2-large"
GPT_2_XL_API_URL = "https://api-inference.huggingface.co/models/gpt2-xl"
BART_API_URL = "https://api-inference.huggingface.co/models/facebook/bart-large-cnn"
headers = {"Authorization": f"Bearer {API_TOKEN}"}


def queryGPT2(payload):
    """
	Raw GPT2 API query
	"""
    data = json.dumps(payload)
    response = requests.request("POST", GPT_2_API_URL, headers=headers, data=data)
    return json.loads(response.content.decode("utf-8"))

def queryBART(payload):
	"""
	Raw BART API query
	"""
	data = json.dumps(payload)
	response = requests.request("POST", BART_API_URL, headers=headers, data=data)
	return json.loads(response.content.decode("utf-8"))

def summarizeBART(text):
	"""
	Returns a Summary with BART
	"""
	payload = {"inputs": text, "parameters": {"do_sample": False},}
	data = queryBART(payload)
	return data[0]["summary_text"]

```

### gmail.py

```python
import os
import pickle
import requests
import json
import contextlib
import sys
import random
from dotenv import load_dotenv
from os.path import join, dirname
# Flask
from flask import Flask, jsonify, request
from flask_cors import CORS
# Firebase
import firebase_admin
from firebase_admin import firestore
from firebase_admin import credentials
# Gmail API utils
from googleapiclient.http import BatchHttpRequest
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow, Flow
from google.auth.transport.requests import Request
# for encoding/decoding messages in base64
from base64 import urlsafe_b64decode, urlsafe_b64encode
# for dealing with attachement MIME types
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
from email.mime.audio import MIMEAudio
from email.mime.base import MIMEBase
from mimetypes import guess_type as guess_mime_type


dotenv_path = join(dirname(__file__), '.env')
load_dotenv(dotenv_path)



REDIRECT_URI = os.environ.get("REDIRECT_URI")
VERBOSE = True # turn off for production

class DummyFile(object):
	def write(self, x): pass

@contextlib.contextmanager
def nostdout():
	save_stdout = sys.stdout
	sys.stdout = DummyFile()
	yield
	sys.stdout = save_stdout
	# pass

# Initialize Firestore DB
cred = credentials.Certificate('firebase.json')
firebase_admin.initialize_app(cred)
db = firestore.client()


# Request all access (permission to read/send/receive emails, manage the inbox, and more)
SCOPES = ['https://mail.google.com/']
our_email = 'your_gmail@gmail.com'

# Creates a service object and returns it
def gmail_authenticate(user_id='token'):
	print("authenticating")
	creds = None
	# the file token.pickle stores the user's access and refresh tokens, and is
	# created automatically when the authorization flow completes for the first time
	if os.path.exists(f'./tokens/{user_id}.pickle'):
		with open(f'./tokens/{user_id}.pickle', "rb") as token:
			creds = pickle.load(token)
	# if there are no (valid) credentials available, let the user log in.
	if not creds or not creds.valid:
		if creds and creds.expired and creds.refresh_token and False:
			creds.refresh(Request())
		else:
			flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)
			creds = flow.run_local_server(port=0) #TODO: Make this not local
		# save the credentials for the next run
		with open(f'./tokens/{user_id}.pickle', "wb") as token:
			pickle.dump(creds, token)
	return build('gmail', 'v1', credentials=creds)


def new_auth(user_id):
	flow = Flow.from_client_secrets_file("credentials.json", scopes=SCOPES, redirect_uri=f'{REDIRECT_URI}/{user_id}/')
	auth_url = flow.authorization_url()
	return auth_url

# Adds the attachment with the given filename to the given message
def add_attachment(message, filename):
	content_type, encoding = guess_mime_type(filename)
	if content_type is None or encoding is not None:
		content_type = 'application/octet-stream'
	main_type, sub_type = content_type.split('/', 1)
	if main_type == 'text':
		fp = open(filename, 'rb')
		msg = MIMEText(fp.read().decode(), _subtype=sub_type)
		fp.close()
	elif main_type == 'image':
		fp = open(filename, 'rb')
		msg = MIMEImage(fp.read(), _subtype=sub_type)
		fp.close()
	elif main_type == 'audio':
		fp = open(filename, 'rb')
		msg = MIMEAudio(fp.read(), _subtype=sub_type)
		fp.close()
	else:
		fp = open(filename, 'rb')
		msg = MIMEBase(main_type, sub_type)
		msg.set_payload(fp.read())
		fp.close()
	filename = os.path.basename(filename)
	msg.add_header('Content-Disposition', 'attachment', filename=filename)
	message.attach(msg)

def build_message(destination, obj, body, attachments=[]):
	if not attachments: # no attachments given
		message = MIMEText(body)
		message['to'] = destination
		message['from'] = our_email
		message['subject'] = obj
	else:
		message = MIMEMultipart()
		message['to'] = destination
		message['from'] = our_email
		message['subject'] = obj
		message.attach(MIMEText(body))
		for filename in attachments:
			add_attachment(message, filename)
	return {'raw': urlsafe_b64encode(message.as_bytes()).decode()}

def send_message(service, destination, obj, body, attachments=[]):
	return service.users().messages().send(
	  userId="me",
	  body=build_message(destination, obj, body, attachments)
	).execute()

def search_messages(service, query):
	result = service.users().messages().list(userId='me',q=query).execute()
	messages = [ ]
	if 'messages' in result:
		messages.extend(result['messages'])
	while 'nextPageToken' in result:
		print("loading messages. . .")
		page_token = result['nextPageToken']
		result = service.users().messages().list(userId='me',q=query, pageToken=page_token).execute()
		if 'messages' in result:
			messages.extend(result['messages'])
	return messages

# utility functions
def get_size_format(b, factor=1024, suffix="B"):
	"""
	Scale bytes to its proper byte format
	e.g:
		1253656 => '1.20MB'
		1253656678 => '1.17GB'
	"""
	for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]:
		if b < factor:
			return f"{b:.2f}{unit}{suffix}"
		b /= factor
	return f"{b:.2f}Y{suffix}"


def clean(text):
	# clean text for creating a folder
	return "".join(c if c.isalnum() else "_" for c in text)



#FIXME: This is a very jank solution to passing data from parse_parts LOL
bodyAttach = {}
bodyAttach['Attachments'] = []
bodyAttach['bodyText'] = None
bodyAttach['bodyHTML'] = None

def parse_parts(service, parts, folder_name, message, download=False, attachmentData=False):
	"""
	Utility function that parses the content of an email partition
	"""

	if parts:
		for part in parts:
			filename = part.get("filename")
			mimeType = part.get("mimeType")
			body = part.get("body")
			data = body.get("data")
			file_size = body.get("size")
			part_headers = part.get("headers")

			if part.get("parts"):
				# recursively call this function when we see that a part
				# has parts inside
				parse_parts(service, part.get("
[truncated — 7026 more characters]
```

### src/Api.js

```javascript
// https://matcha-v0.onrender.com/full-database



async function updateDatabase() {
	fetch('https://matcha-v0.onrender.com/full-database')
	.then(response => response.json())
	.then(data => console.log(data)
	
	
	);
  }

export default updateDatabase;
```

[16 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]