# Project export: Mailman Extension

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: Cal Hacks 12.0
- Tagline: Reading emails is boring, you don’t need to. Imagine an extension that is your personal assistant and lets you know about emails like an assistant. You can even ask it for specific questions.
- Devpost: https://devpost.com/software/mailman-extension
- GitHub: https://github.com/adityarp3/mailman-ext
- Team: 1 GitHub contributor(s) — unknown (5 commits)

## Devpost submission (written by the team)

### Inspiration

I get a lot of emails a day and want to only read the important ones on some days.

### What it does

It takes all your recent unread emails, and becomes your AI assistant for sorting by importance, telling you what the email is about and answer questions about the emails.

### How we built it

Python/JS/Flask framework/Gmail API/Gemini API

### Challenges we ran into

UI - I am not good with JavaScript.

### Accomplishments we're proud of

It works exactly how I hoped for.

### What's next

Hoping to deploy it for everyone to use soon.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 5 recognized source files, 31 KB.
- Flask (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (9 of 9)

```
.gitignore
.vscode/settings.json
backend/app.py
extension/background.js
extension/manifest.json
extension/popup.html
extension/popup.js
README.md
requirements.txt
```

### Dependencies

- requirements.txt: flask@==3.0.0, flask-cors@==4.0.0, google-api-python-client@==2.110.0, google-auth@==2.25.2, google-auth-httplib2@==0.2.0, google-auth-oauthlib@==1.2.0, gunicorn@==23.0.0, python-dotenv@==1.1.1, requests@==2.31.0

### Recent commits (newest first)

- Delete extra files
- Update requirements.txt
- Initial Commit with fixed .gitignore
- Initial Commit with .gitignore
- Initial Commit

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

### requirements.txt

```
flask==3.0.0
flask-cors==4.0.0
google-auth==2.25.2
google-auth-oauthlib==1.2.0
google-auth-httplib2==0.2.0
google-api-python-client==2.110.0
requests==2.31.0
python-dotenv==1.1.1
gunicorn==23.0.0

```

### backend/app.py

```python
from flask import Flask, jsonify, request
from flask_cors import CORS
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
import os
import base64
import requests
import json
import time 
from dotenv import load_dotenv

load_dotenv()

app = Flask(__name__)
CORS(app) 

SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']

GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")

GEMINI_MODEL_ID = "gemini-2.5-flash" 
GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/{GEMINI_MODEL_ID}:generateContent"


def get_gmail_service():
    """
    Authenticate and return Gmail API service using the Bearer token
    provided by the client (browser extension).
    """
    auth_header = request.headers.get('Authorization')
    if not auth_header:
        raise Exception('Authorization header is missing')

    try:
        token_type, access_token = auth_header.split(' ')
        if token_type.lower() != 'bearer':
            raise Exception('Authorization header must start with Bearer')
        
        creds = Credentials(access_token, scopes=SCOPES)
        
        return build('gmail', 'v1', credentials=creds)

    except Exception as e:
        print(f"Error parsing auth header: {e}")
        raise Exception(f'Invalid Authorization header: {e}')


def get_email_body(payload):
    """Extract email body from message payload"""
    if 'parts' in payload:
        for part in payload['parts']:
            if part['mimeType'] == 'text/plain':
                data = part['body'].get('data', '')
                if data:
                    return base64.urlsafe_b64decode(data).decode('utf-8')
    else:
        data = payload.get('body', {}).get('data', '')
        if data:
            return base64.urlsafe_b64decode(data).decode('utf-8')
    return ""

def rule_based_analysis(subject, sender, body):
    """Fallback rule-based analysis if Gemini fails or if email is pre-filtered."""
    priority = 5
    reason = "Default priority (AI analysis failed or quota exceeded)"
    
    sender_lower = sender.lower()
    

    low_priority_keywords = ['noreply', 'marketing', 'promo', 'unsubscribe', 'coupon', 'newsletter', 
                             'advertisement', 'deal', 'offer', 'weekly digest', 'sale', 'save now']
    
    if any(word in sender_lower for word in low_priority_keywords) or any(word in subject.lower() for word in low_priority_keywords):
        priority = 2
        reason = "Automated/promotional email filter hit"
        return {
            "summary": f"Email from {sender}: {subject}",
            "priority": priority,
            "reason": reason
        }

    if any(word in sender_lower for word in ['gov', 'government', 'irs', 'court', 'legal', '.gov']):
        priority = 9
        reason = "Government/legal sender"
    elif any(word in sender_lower for word in ['boss', 'manager', 'ceo', 'director']):
        priority = 8
        reason = "Management communication"
    elif any(word in sender_lower for word in ['teacher', 'professor', 'instructor', '.edu']):
        priority = 7
        reason = "Educational authority"
    
    subject_lower = subject.lower()
    urgent_keywords = ['urgent', 'immediate', 'action required', 'deadline', 'asap', 'emergency', 'important']
    if any(word in subject_lower for word in urgent_keywords):
        priority = min(priority + 2, 10)
        reason = f"{reason} + urgent keywords"
    
    body_lower = body[:200].lower()
    if any(word in body_lower for word in ['due date', 'overdue', 'payment', 'suspended', 'expires']):
        priority = min(priority + 1, 10)
    
    summary = f"Email from {sender}: {subject}"
    if len(summary) > 100:
        summary = summary[:97] + "..."
    
    return {
        "summary": summary,
        "priority": priority,
        "reason": reason
    }


def analyze_email_with_gemini(subject, sender, body, date, max_retries=3):
    """Use Google Gemini to analyze and prioritize email with retry logic and pre-check filter."""
    
    low_priority_keywords = ['unsubscribe', 'coupon', 'newsletter', 'advertisement', 'promo', 'marketing', 
                             'noreply', 'deal', 'offer', 'weekly digest', 'sale', 'save now']
    
    sender_lower = sender.lower()
    subject_lower = subject.lower()

    if any(word in sender_lower for word in low_priority_keywords) or any(word in subject_lower for word in low_priority_keywords):
        print(f"Skipping AI analysis for potential promo/spam: {subject}")
        return rule_based_analysis(subject, sender, body)
    
    if not GEMINI_API_KEY:
        print("Falling back to rule-based analysis: GEMINI_API_KEY not found.")
        return rule_based_analysis(subject, sender, body)
    
    
    system_instruction = (
        "You are an email prioritization expert. Your primary goal is to identify emails "
        "that require a *personal, timely response* or contain *critical personal or professional information*. "
        "Be extremely critical of any email that resembles marketing, automated reports, "
        "or social media notifications. Only assign a priority score of 7 or higher if the email "
        "demands immediate human action or contains legally/financially important content."
    )
    
    prompt = f"""{system_instruction}

--- END OF INSTRUCTIONS ---

Analyze this email and provide a brief summary, priority score, and reason.

Email Details:
From: {sender}
Date: {date}
Subject: {subject}
Body: {body[:1000]}

Respond ONLY with valid JSON in this exact format:
{{
    "summary": "brief 1-2 sentence summary",
    "priority": 8,
    "reason": "explanation for priority score"
}}
"""

    for attempt in range(max_retries):
        try:
            response = requests.post(
                f"{GEMINI_API_URL}?key={GEMINI_API_KEY}",
                json={
                    "contents": [
                        {
                            "role": "user",
                            "parts": [{"t
[truncated — 8540 more characters]
```

### extension/popup.html

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    body {
      width: 450px;
      max-height: 600px;
      margin: 0;
      padding: 0;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    }

    .header {
      padding: 20px;
      color: white;
      text-align: center;
    }

    .header h1 {
      margin: 0 0 5px 0;
      font-size: 24px;
      font-weight: 600;
    }

    .header p {
      margin: 0;
      opacity: 0.9;
      font-size: 14px;
    }

    .content {
      background: white;
      border-radius: 20px 20px 0 0;
      padding: 20px;
      max-height: 480px;
      overflow-y: auto;
    }

    .loading {
      text-align: center;
      padding: 40px 20px;
      color: #666;
    }

    .spinner {
      border: 3px solid #f3f3f3;
      border-top: 3px solid #667eea;
      border-radius: 50%;
      width: 40px;
      height: 40px;
      animation: spin 1s linear infinite;
      margin: 0 auto 15px;
    }

    @keyframes spin {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }

    .email-card {
      background: #f8f9fa;
      border-radius: 12px;
      padding: 15px;
      margin-bottom: 12px;
      border-left: 4px solid;
      transition: transform 0.2s, box-shadow 0.2s;
      cursor: pointer;
    }

    .email-card:hover {
      transform: translateY(-2px);
      box-shadow: 0 4px 12px rgba(0,0,0,0.1);
    }

    .email-card.priority-high {
      border-left-color: #e74c3c;
      background: #fff5f5;
    }

    .email-card.priority-medium {
      border-left-color: #f39c12;
      background: #fffbf5;
    }

    .email-card.priority-low {
      border-left-color: #3498db;
      background: #f5f9ff;
    }

    .priority-badge {
      display: inline-block;
      padding: 4px 10px;
      border-radius: 12px;
      font-size: 11px;
      font-weight: 600;
      text-transform: uppercase;
      margin-bottom: 8px;
    }

    .priority-high .priority-badge {
      background: #e74c3c;
      color: white;
    }

    .priority-medium .priority-badge {
      background: #f39c12;
      color: white;
    }

    .priority-low .priority-badge {
      background: #3498db;
      color: white;
    }

    .email-subject {
      font-weight: 600;
      font-size: 15px;
      margin-bottom: 5px;
      color: #2c3e50;
    }

    .email-sender {
      font-size: 12px;
      color: #7f8c8d;
      margin-bottom: 8px;
    }

    .email-summary {
      font-size: 13px;
      color: #34495e;
      line-height: 1.5;
      margin-bottom: 8px;
    }

    .email-reason {
      font-size: 11px;
      color: #95a5a6;
      font-style: italic;
    }

    .refresh-btn {
      width: 100%;
      padding: 12px;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      border: none;
      border-radius: 8px;
      font-size: 14px;
      font-weight: 600;
      cursor: pointer;
      margin-top: 10px;
      transition: opacity 0.2s;
    }

    .refresh-btn:hover {
      opacity: 0.9;
    }

    .refresh-btn:disabled {
      opacity: 0.5;
      cursor: not-allowed;
    }

    .no-emails {
      text-align: center;
      padding: 40px 20px;
      color: #7f8c8d;
    }

    .no-emails svg {
      width: 60px;
      height: 60px;
      margin-bottom: 15px;
      opacity: 0.5;
    }

    .error {
      background: #ffe6e6;
      color: #c0392b;
      padding: 15px;
      border-radius: 8px;
      margin-bottom: 15px;
      font-size: 13px;
    }

    .chat-section {
      margin-top: 20px;
      padding-top: 20px;
      border-top: 2px solid #e0e0e0;
    }

    .chat-header {
      display: flex;
      align-items: center;
      margin-bottom: 15px;
      font-weight: 600;
      color: #667eea;
    }

    .chat-header svg {
      width: 20px;
      height: 20px;
      margin-right: 8px;
    }

    .chat-messages {
      max-height: 200px;
      overflow-y: auto;
      margin-bottom: 15px;
      padding: 10px;
      background: #f8f9fa;
      border-radius: 8px;
      display: none;
    }

    .chat-messages.active {
      display: block;
    }

    .chat-message {
      margin-bottom: 12px;
      padding: 10px;
      border-radius: 8px;
      font-size: 13px;
      line-height: 1.5;
    }

    .chat-message.user {
      background: #667eea;
      color: white;
      margin-left: 20px;
    }

    .chat-message.assistant {
      background: white;
      color: #2c3e50;
      margin-right: 20px;
      border: 1px solid #e0e0e0;
    }

    .chat-input-container {
      display: flex;
      gap: 8px;
    }

    .chat-input {
      flex: 1;
      padding: 10px 15px;
      border: 2px solid #e0e0e0;
      border-radius: 8px;
      font-size: 14px;
      font-family: inherit;
      transition: border-color 0.2s;
    }

    .chat-input:focus {
      outline: none;
      border-color: #667eea;
    }

    .chat-send-btn {
      padding: 10px 20px;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      border: none;
      border-radius: 8px;
      font-size: 14px;
      font-weight: 600;
      cursor: pointer;
      transition: opacity 0.2s;
    }

    .chat-send-btn:hover:not(:disabled) {
      opacity: 0.9;
    }

    .chat-send-btn:disabled {
      opacity: 0.5;
      cursor: not-allowed;
    }

    .suggested-questions {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      margin-bottom: 15px;
    }

    .suggested-question {
      padding: 6px 12px;
      background: #f0f0f0;
      border: 1px solid #d0d0d0;
      border-radius: 16px;
      font-size: 11px;
      cursor: pointer;
      transition: all 0.2s;
    }

    .suggested-question:hover {
      background: #667eea;
      color: white;
      border-color: #667eea;
    }
  </style>
</head>
<body>
  <div class="header">
    <h1>Mailman</h1>
    <p>Personal Email AI Assistant.</p>
  </div>

  <div class="
[truncated — 1658 more characters]
```

### extension/popup.js

```javascript
const API_URL = 'http://localhost:5000';

let currentEmails = [];

function getAuthToken() {
  return new Promise((resolve, reject) => {
    chrome.identity.getAuthToken({ interactive: true }, (token) => {
      if (chrome.runtime.lastError || !token) {
        console.error("DEBUG: Token Retrieval Failed!", chrome.runtime.lastError);
        reject(new Error(chrome.runtime.lastError.message || 'Authentication failed. Please verify manifest.json.'));
      } else {
        console.log("DEBUG: Token Retrieved Successfully.");
        resolve(token);
      }
    });
  });
}

async function fetchEmails(token) {
  console.log("DEBUG: fetchEmails called. Token length:", token.length);
  
  const loading = document.getElementById('loading');
  const emailsContainer = document.getElementById('emails');
  const refreshBtn = document.getElementById('refreshBtn');
  const chatSection = document.getElementById('chatSection');
  const chatInput = document.getElementById('chatInput');
  const chatSendBtn = document.getElementById('chatSendBtn');

  loading.style.display = 'block';
  emailsContainer.style.display = 'none';
  refreshBtn.disabled = true;
  chatSection.style.display = 'none';

  try {
    if (!token || token.length < 10) {
        throw new Error("Missing Auth Token. Cannot fetch emails.");
    }
    
    const response = await fetch(`${API_URL}/api/unread-emails`, {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });
    
    const data = await response.json();

    loading.style.display = 'none';
    emailsContainer.style.display = 'block';
    refreshBtn.style.display = 'block';
    refreshBtn.disabled = false;

    if (data.error) {
      emailsContainer.innerHTML = `
        <div class="error">
          <strong>Error from Backend:</strong> ${data.error}
          <p>This may mean your token expired or the Gemini API key is missing on the server.</p>
        </div>
      `;
      return;
    }

    if (!data.emails || data.emails.length === 0) {
      emailsContainer.innerHTML = `
        <div class="no-emails">
          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path d="M22 12h-4l-3 9L9 3l-3 9H2"></path>
          </svg>
          <h3>All caught up!</h3>
          <p>No unread emails at the moment.</p>
        </div>
      `;
      return;
    }

    currentEmails = data.emails;

    emailsContainer.innerHTML = data.emails.map(email => {
      const priorityClass = email.priority >= 7 ? 'priority-high' : 
                            email.priority >= 4 ? 'priority-medium' : 
                            'priority-low';
      
      const priorityLabel = email.priority >= 7 ? 'Urgent' : 
                            email.priority >= 4 ? 'Important' : 
                            'Normal';

      return `
        <div class="email-card ${priorityClass}" data-id="${email.id}">
          <span class="priority-badge">
            ${priorityLabel} (${email.priority}/10)
          </span>
          <div class="email-subject">${escapeHtml(email.subject)}</div>
          <div class="email-sender">From: ${escapeHtml(email.sender)}</div>
          <div class="email-summary">${escapeHtml(email.summary)}</div>
          <div class="email-reason">📌 ${escapeHtml(email.reason)}</div>
        </div>
      `;
    }).join('');

    chatSection.style.display = 'block';
    chatInput.disabled = false;
    chatSendBtn.disabled = false;

    document.querySelectorAll('.email-card').forEach(card => {
      card.addEventListener('click', async () => {
        const emailId = card.dataset.id;
        
        try {
          await fetch(`${API_URL}/api/mark-read`, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'Authorization': `Bearer ${token}`
            },
            body: JSON.stringify({ email_id: emailId })
          });
          
          card.style.opacity = '0.5';
          setTimeout(() => {
            card.remove();
            
            currentEmails = currentEmails.filter(e => e.id !== emailId);
            
            if (document.querySelectorAll('.email-card').length === 0) {
              main();
            }
          }, 300);
        } catch (error) {
          console.error('Error marking email as read:', error);
        }
      });
    });

  } catch (error) {
    loading.style.display = 'none';
    emailsContainer.style.display = 'block';
    emailsContainer.innerHTML = `
      <div class="error">
        <strong>Client-Side Error:</strong> ${error.message}
        <br><br>
        <small>If this is a token error, please close the popup and try again.</small>
      </div>
    `;
    refreshBtn.style.display = 'block';
    refreshBtn.disabled = false;
  }
}

async function askQuestion(question) {
  const chatMessages = document.getElementById('chatMessages');
  const chatInput = document.getElementById('chatInput');
  const chatSendBtn = document.getElementById('chatSendBtn');

  if (!question.trim()) return;

  chatMessages.classList.add('active');

  const userMessage = document.createElement('div');
  userMessage.className = 'chat-message user';
  userMessage.textContent = question;
  chatMessages.appendChild(userMessage);

  chatInput.value = '';
  chatInput.disabled = true;
  chatSendBtn.disabled = true;

  const loadingMessage = document.createElement('div');
  loadingMessage.className = 'chat-message assistant';
  loadingMessage.textContent = 'Thinking...';
  chatMessages.appendChild(loadingMessage);

  chatMessages.scrollTop = chatMessages.scrollHeight;

  // Log the request payload being sent
  console.log("DEBUG: Sending question to /api/ask-question. Emails count:", currentEmails.length);

  try {
    const response = await fetch(`${API_URL}/api/ask-question`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        ques
[truncated — 3139 more characters]
```