main 0946e1747eff cached
16 files
72.3 KB
16.6k tokens
64 symbols
1 requests
Download .txt
Repository: tonykipkemboi/crewai-gmail-automation
Branch: main
Commit: 0946e1747eff
Files: 16
Total size: 72.3 KB

Directory structure:
gitextract_9jx_yv_y/

├── .env_example
├── .gitignore
├── LICENSE
├── README.md
├── knowledge/
│   └── user_preference.txt
├── pyproject.toml
└── src/
    └── gmail_crew_ai/
        ├── __init__.py
        ├── config/
        │   ├── agents.yaml
        │   └── tasks.yaml
        ├── crew.py
        ├── main.py
        ├── models.py
        └── tools/
            ├── __init__.py
            ├── date_tools.py
            ├── gmail_tools.py
            └── slack_tool.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .env_example
================================================
# OpenAI Model
MODEL=openai/gpt-4o-mini
OPENAI_API_KEY=your_openai_api_key

# Gmail credentials
EMAIL_ADDRESS=your_email_address@gmail.com
APP_PASSWORD=your_app_password

# Slack Webhook URL
SLACK_WEBHOOK_URL=your_slack_webhook_url

================================================
FILE: .gitignore
================================================
# Environment variables
.env
.env.*

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Virtual Environment
venv/
.venv/
env/
ENV/

# IDE
.idea/
.vscode/
*.swp
*.swo

# Project specific
output
*.log

# OS specific
.DS_Store
Thumbs.db



================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2025 Tony Kipkemboi

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Gmail Automation with CrewAI 📧✨

[![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCApiD66gf36M9hZanbjgNaw?style=social)](https://www.youtube.com/@tonykipkemboi)
[![GitHub followers](https://img.shields.io/github/followers/tonykipkemboi?style=social)](https://github.com/tonykipkemboi)
[![Twitter Follow](https://img.shields.io/twitter/follow/tonykipkemboi?style=social)](https://twitter.com/tonykipkemboi)
[![LinkedIn](https://img.shields.io/badge/LinkedIn-Connect-blue?style=flat&logo=linkedin)](https://www.linkedin.com/in/tonykipkemboi/)

Gmail Automation with CrewAI is an intelligent email management system that uses AI agents to categorize, organize, respond to, and clean up your Gmail inbox automatically.

![Gmail Automation](./assets/gmail-automation.jpg)

## ✨ Features

![Stars](https://img.shields.io/github/stars/tonykipkemboi/crewai-gmail-automation?style=social)
![Last Commit](https://img.shields.io/github/last-commit/tonykipkemboi/crewai-gmail-automation) 
![Status](https://img.shields.io/badge/Status-Active-brightgreen)

- **📋 Email Categorization**: Automatically categorizes emails into specific types (newsletters, promotions, personal, etc.)
- **🔔 Priority Assignment**: Assigns priority levels (HIGH, MEDIUM, LOW) based on content and sender with strict classification rules
- **🏷️ Smart Organization**: Applies Gmail labels and stars based on categories and priorities
- **💬 Automated Responses**: Generates draft responses for important emails that need replies
- **📱 Slack Notifications**: Sends creative notifications for high-priority emails
- **🧹 Intelligent Cleanup**: Safely deletes low-priority emails based on age and category
- **🎬 YouTube Content Protection**: Special handling for YouTube-related emails
- **🗑️ Trash Management**: Automatically empties trash to free up storage space
- **🧵 Thread Awareness**: Recognizes and properly handles email threads


## 🚀 Installation

```bash
# Clone the repository
git clone https://github.com/tonykipkemboi/crewai-gmail-automation.git
cd crewai-gmail-automation

# Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate

# Install dependencies
crewai install
```

## ⚙️ Configuration

1. Create a `.env` file in the root directory with the following variables:

```
# Choose your LLM provider
# OpenAI (Recommended)
MODEL=openai/gpt-4o-mini
OPENAI_API_KEY=your_openai_api_key

# Or Gemini
# MODEL=gemini/gemini-2.0-flash
# GEMINI_API_KEY=your_gemini_api_key

# Or Ollama  (Note: May have compatibility issues with tool calling)
# Download the model from https://ollama.com/library
# MODEL=ollama/llama3-groq-tool-use # use ones that have tool calling capabilities

# Gmail credentials
EMAIL_ADDRESS=your_email@gmail.com
APP_PASSWORD=your_app_password

# Optional: Slack notifications
SLACK_WEBHOOK_URL=your_slack_webhook_url
```

<details>
<summary><b>🔑 How to create a Gmail App Password</b></summary>

1. Go to your Google Account settings at [myaccount.google.com](https://myaccount.google.com/)
2. Select **Security** from the left navigation panel
3. Under "Signing in to Google," find and select **2-Step Verification** (enable it if not already enabled)
4. Scroll to the bottom and find **App passwords**
5. Select **Mail** from the "Select app" dropdown
6. Select **Other (Custom name)** from the "Select device" dropdown
7. Enter `Gmail CrewAI` as the name
8. Click **Generate**
9. Copy the 16-character password that appears (spaces will be removed automatically)
10. Paste this password in your `.env` file as the `APP_PASSWORD` value
11. Click **Done**

**Note**: App passwords can only be created if you have 2-Step Verification enabled on your Google account.
</details>

<details>
<summary><b>🔗 How to create a Slack Webhook URL</b></summary>

1. Go to [api.slack.com/apps](https://api.slack.com/apps)
2. Click **Create New App**
3. Select **From scratch**
4. Enter `Gmail Notifications` as the app name
5. Select your workspace and click **Create App**
6. In the left sidebar, find and click on **Incoming Webhooks**
7. Toggle the switch to **Activate Incoming Webhooks**
8. Click **Add New Webhook to Workspace**
9. Select the channel where you want to receive notifications
10. Click **Allow**
11. Find the **Webhook URL** section and copy the URL that begins with `https://hooks.slack.com/services/`
12. Paste this URL in your `.env` file as the `SLACK_WEBHOOK_URL` value

**Customizing your Slack app (optional):**
1. Go to **Basic Information** in the left sidebar
2. Scroll down to **Display Information**
3. Add an app icon and description
4. Click **Save Changes**

**Note**: You need admin permissions or the ability to install apps in your Slack workspace.
</details>

## 📧 How It Works

This application uses the IMAP (Internet Message Access Protocol) to securely connect to your Gmail account and manage your emails. Here's how it works:

<details>
<summary><b>🔄 IMAP Connection Process</b></summary>

1. **Secure Connection**: The application establishes a secure SSL connection to Gmail's IMAP server (`imap.gmail.com`).

2. **Authentication**: It authenticates using your email address and app password (not your regular Google password).

3. **Mailbox Access**: Once authenticated, it can access your inbox and other mailboxes to:
   - Read unread emails
   - Apply labels
   - Move emails to trash
   - Save draft responses

4. **Safe Disconnection**: After each operation, the connection is properly closed to maintain security.

IMAP allows the application to work with your emails while they remain on Google's servers, unlike POP3 which would download them to your device. This means you can still access all emails through the regular Gmail interface.

**Security Note**: Your credentials are only stored locally in your `.env` file and are never shared with any external services.
</details>

## 🔍 Usage

Run the application with:

```bash
crewai run
```

You'll be prompted to enter the number of emails to process (default is 5).

The application will:
1. 📥 Fetch your unread emails
2. 🔎 Categorize them by type and priority
3. ⭐ Apply appropriate labels and stars
4. ✏️ Generate draft responses for important emails
5. 🔔 Send Slack notifications for high-priority items
6. 🗑️ Clean up low-priority emails based on age
7. 🧹 Empty the trash to free up storage space


## 🌟 Special Features

- **📅 Smart Deletion Rules**: 
  - Promotions older than 2 days are automatically deleted
  - Newsletters older than 7 days (unless HIGH priority) are deleted
  - Shutterfly emails are always deleted regardless of age
  - Receipts and important documents are archived instead of deleted

- **🎬 YouTube Protection**: All YouTube-related emails are preserved and marked as READ_ONLY (you'll respond directly on YouTube)

- **✍️ Smart Response Generation**: Responses are tailored to the email context and include proper formatting

- **💡 Creative Slack Notifications**: Fun, attention-grabbing notifications for important emails

- **🧵 Thread Handling**: Properly tracks and manages email threads to maintain conversation context

## 👥 Contributing

Contributions are welcome! Please feel free to submit a [Pull Request](https://github.com/tonykipkemboi/crewai-gmail-automation/pulls).

## 📄 License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## 📚 References

- [CrewAI](https://github.com/crewAIInc/crewAI/)
- [IMAP Protocol for Gmail](https://support.google.com/mail/answer/7126229)
- [Slack API](https://api.slack.com/messaging/webhooks)
- [Ollama](https://ollama.com/library)
- [Gemini](https://ai.google.com/gemini-api)
- [OpenAI](https://openai.com/api/)



================================================
FILE: knowledge/user_preference.txt
================================================
User name is John Doe.
User is an AI Engineer.
User is interested in AI Agents.
User is based in San Francisco, California.


================================================
FILE: pyproject.toml
================================================
[project]
name = "gmail_crew_ai"
version = "0.1.0"
description = "gmail-crew-ai using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.13"
dependencies = [
    "bs4>=0.0.2",
    "crewai[tools]>=0.102.0,<1.0.0",
]

[project.scripts]
gmail_crew_ai = "gmail_crew_ai.main:run"
run_crew = "gmail_crew_ai.main:run"
train = "gmail_crew_ai.main:train"
replay = "gmail_crew_ai.main:replay"
test = "gmail_crew_ai.main:test"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.crewai]
type = "crew"


================================================
FILE: src/gmail_crew_ai/__init__.py
================================================


================================================
FILE: src/gmail_crew_ai/config/agents.yaml
================================================
categorizer:
  role: >
    Email Categorizer and Prioritizer
  goal: >
    Analyze and categorize unread emails based on urgency, importance, and required actions, 
    ensuring critical communications are identified and properly prioritized.
  backstory: >
    You are an expert email manager with years of experience in handling high-volume inboxes. 
    You excel at quickly identifying urgent matters, understanding email context, and determining 
    appropriate priority levels. You're particularly skilled at recognizing the true importance 
    of messages beyond just their subject lines.
  verbose: true
  memory: true

organizer:
  role: >
    Email Organization Specialist
  goal: >
    Apply Gmail's organizational features (stars, labels, priority markers) consistently and 
    effectively based on email categorization, maintaining a well-structured and easily navigable inbox.
  backstory: >
    You are a Gmail power user and productivity expert who understands how to leverage Gmail's 
    organizational features to their fullest potential. You've helped numerous executives maintain 
    pristine inboxes and have developed a systematic approach to email organization that ensures 
    important messages are easily findable and properly highlighted.
  verbose: true
  memory: true

response_generator:
  role: >
    Email Response Draft Generator
  goal: >
    Create professional, context-aware email responses that effectively address the sender's needs 
    while maintaining appropriate tone and including all necessary information.
  backstory: >
    You are a skilled professional writer with extensive experience in business communication. 
    You excel at crafting responses that are clear, concise, and appropriate for the context. 
    You understand the importance of maintaining professional relationships through effective 
    email communication and know when to include additional resources like calendar links for meetings.
  verbose: true
  memory: true

notifier:
  role: >
    Email Notification Specialist
  goal: >
    Monitor important emails and send timely notifications to Slack for high and medium priority messages.
  backstory: >
    You are a vigilant notification specialist who ensures that important communications are never missed. 
    You understand which emails require immediate attention and can summarize complex information into 
    clear, actionable notifications. Your role is to bridge the gap between email and Slack, ensuring 
    that urgent matters are promptly brought to attention.
  verbose: true
  memory: true

cleaner:
  role: >
    Email Cleanup Specialist
  goal: >
    Identify and delete low-priority emails that are cluttering the inbox while ensuring 
    no important communications are lost.
  backstory: >
    You are a meticulous digital organizer who specializes in email management. You have a 
    keen eye for distinguishing between emails that need to be preserved and those that can 
    be safely removed. You understand the importance of maintaining a clean inbox while being 
    extremely careful not to delete anything of value.
  verbose: true
  memory: true

trash_cleaner:
  role: "Email Trash Cleanup Specialist"
  goal: "Empty the Gmail trash folder to free up storage space"
  backstory: "You are an expert at managing email storage efficiently. Your job is to help users free up space by emptying the trash folder when needed."

================================================
FILE: src/gmail_crew_ai/config/tasks.yaml
================================================
categorization_task:
  description: >
    First, read the fetched emails from the file at 'output/fetched_emails.json' using the `FileReadTool`.
    
    For each email, analyze the content and categorize as follows:

    1. Category (choose one):
       - NEWSLETTERS
       - PROMOTIONS
       - PERSONAL
       - GITHUB
       - YOUTUBE
       - RECEIPTS_INVOICES
       - OTHER

    2. Priority (choose one):
      IMPORTANT - BE VERY STRICT WITH PRIORITY LEVELS:
      - HIGH: ONLY for urgent matters requiring immediate attention or response
      - MEDIUM: ONLY for important but not urgent communications from real people
         - DO NOT mark promotional content, newsletters, or automated notifications as MEDIUM
         - DO NOT mark Shutterfly or other promotional emails as MEDIUM - they should be LOW
         - Only actual communication from real people can be MEDIUM
      - LOW

    3. Required Action (choose one):
       - REPLY
       - READ_ONLY
       - TASK
       - IGNORE

    IMPORTANT YOUTUBE EMAIL RULES:
    - ANY email from youtube.com domains MUST be categorized as YOUTUBE
    - ANY email containing "YouTube" in the subject or sender MUST be categorized as YOUTUBE
    - YouTube emails about comments should be HIGH priority but require READ_ONLY action
      (the user will reply directly on YouTube, not via email)
    
    IMPORTANT FORMAT INSTRUCTIONS:
    Your final answer must be a valid JSON object with these exact fields(example without the quotes):
      "email_id": "The email's unique identifier",
      "subject": "The email's subject line",
      "sender": "The email sender",
      "category": "The category you assigned",
      "priority": "The priority you assigned",
      "required_action": "The action needed",
      "date": "The email date in YYYY-MM-DD format",
      "age_days": "The age of the email in days"
    
    Do not include any explanations, thoughts, or additional text outside the JSON object.
    Do not use markdown formatting or code blocks.
    Just return the raw JSON object.
  expected_output: >
    A JSON object with email_id, subject, category, priority, and required_action fields.
  agent: categorizer
  output_file: output/categorization_report.json

organization_task:
  description: >
    First, read the categorization report from 'output/categorization_report.json' using the `FileReadTool`.
    
    For each email, organize it using Gmail's priority features with the 'organize_email' tool.

    Use the email content, email domain, and categorization report to determine the correct labels.
    
    Simplified Label Rules:
    
    1. For YOUTUBE emails:
       - Add "YOUTUBE" label
       - Star the email
       - If HIGH priority, also add "URGENT" label
    
    2. For NEWSLETTERS emails:
       - Add "NEWSLETTERS" label
       - No newsletter is HIGH or MEDIUM priority
       - No newsletter requires action
    
    3. For other emails:
       - Add a label matching the category (e.g., "PERSONAL", "GITHUB")
       - If HIGH priority, add "URGENT" label and star the email
       - If MEDIUM priority, add "ACTION_NEEDED" label
    
    Your final answer should be a JSON object with:
    - email_id: The email's ID
    - subject: The email's subject
    - applied_labels: List of labels applied
    - starred: Whether the email was starred (true/false)
    - result: "Success" or error message
  expected_output: >
    A detailed report of how each email was organized, including:
    - The email ID and subject
    - The labels that were applied
    - Whether the email was starred
    - The result of the organization attempt

    The report should include special handling for YOUTUBE and NEWSLETTERS emails to ensure they receive
    the appropriate labels and priority indicators.
  agent: organizer
  context: [categorization_task]
  output_file: output/organization_report.json

response_task:
  description: >
    Based on the categorization report, generate responses ONLY for emails that require action.
    
    Only generate responses for:
    - PERSONAL emails with HIGH or MEDIUM priority
    - DO NOT generate responses for ANY YouTube emails - the user will respond directly on YouTube instead
    
    
    For YouTube comments:
    - Thank them for their comment
    - Answer their question if possible
    - End with "Best regards, Tony Kipkemboi"
    
    IMPORTANT: For the recipient field, extract the username from the comment.
    For example, if the comment is from "@username commented on your video",
    use "username@gmail.com" as the recipient.
    
    Use the 'save_email_draft' tool with:
    - subject: Add "Re: " to the original subject
    - body: Your response
    - recipient: The extracted username + "@gmail.com" or use "user@example.com" if you can't extract it
    
    Your final answer should be a simple list of emails you responded to.
  expected_output: >
    A draft email saved to the drafts folder for each email that requires a response, or 
    "No response needed" for emails to be deleted or ignored. Each draft should follow the 
    specified structure and formatting guidelines. The should be in correct Markdown format without
    any additional text or formatting such as "```" or "```md".
  agent: response_generator
  context: [categorization_task, organization_task]
  output_file: output/response_report.json

notification_task:
  description: >
    Based on the categorization report, send Slack notifications for HIGH priority emails using the 'SlackNotificationTool'.
    
    For each HIGH priority email:
    1. Create a brief summary of the email content
    2. Identify any action needed
    3. Create an attention-grabbing headline
    4. Add an emoji-filled intro
    
    Use the 'SlackNotificationTool' with:
    - subject: The email subject
    - sender: The email sender
    - category: The email category (YOUTUBE, PERSONAL, etc.)
    - priority: HIGH
    - summary: Your brief summary
    - action_needed: What action is needed
    - headline: Your custom headline
    - intro: Your custom intro with emojis
    
    Your final answer should list all notifications sent.
  expected_output: >
    A report of which emails had Slack notifications sent, including their subjects, priorities, and the creative elements used.
    The report should be in correct Markdown format without any additional text or formatting such as "```" or "```md".
  agent: notifier
  context: [categorization_task]
  output_file: output/notification_report.json

cleanup_task:
  description: >
    Based on the categorization report, identify LOW priority emails that are safe to delete.
    
    EMAILS TO ARCHIVE INSTEAD OF DELETE:
    - Receipts and Invoices (archive for tax purposes)
    - Confirmation emails for purchases
    - Travel-related emails with booking information
    - Anything that might be needed for reference later
    
    For these emails, use the 'GmailArchiveTool' instead of deleting them.
    
    NEVER delete:
    - ANY email marked as HIGH or MEDIUM priority
    - ANY email from a personal contact
    - ANY email related to YouTube
    - ANY email less than 5 days old
    
    ALWAYS delete:
    - ANY email with "Shutterfly" in the sender or subject, regardless of categorization
    - ANY email categorized as PROMOTIONS and older than 2 days
    - ANY email categorized as NEWSLETTERS and older than 7 days, unless HIGH priority
    
    The emails already have their age calculated in the "age_days" field.
    An email is less than 5 days old if age_days < 5.
    
    For each email:
    1. Check if it meets the deletion criteria (LOW priority AND not from criteria above)
    2. For emails you decide to delete, document:
       - The email subject and sender
       - The reason for deletion
       - Use the 'GmailDeleteTool' with the email_id and reason

    AFTER processing all emails for deletion:
    3. IMPORTANT: Use the 'empty_gmail_trash' tool to permanently empty the trash folder
       and free up storage space in the Gmail account.

    WORKFLOW:
    1. First, analyze each email one at a time
    2. For emails that should be deleted, use delete_email tool with ONLY the required fields
    3. After processing all emails, use empty_gmail_trash tool
    4. Then compile your final report

    Your final answer should provide a detailed report containing:
    - DELETED EMAILS: For each deleted email, include email_id, subject, sender, category, priority, age, and reason for deletion
    - PRESERVED EMAILS: For each preserved email, include email_id, subject, sender, category, priority, age, and reason for preservation
    - TRASH STATUS: Information about whether the trash was emptied successfully and how many messages were permanently removed
    Also include information about whether the trash was emptied successfully.

    IMPORTANT: When using the 'delete_email' tool, ONLY provide the email_id and reason 
    as a simple JSON object. For example:
    
      "email_id": "123", 
      "reason": "Low priority promotional email"
    
  expected_output: >
    A comprehensive deletion report with three sections:
    1. DELETED EMAILS: Full details of each deleted email including subject, sender, id, category, priority, age and deletion reason
    2. PRESERVED EMAILS: Full details of each preserved email including the reason for preservation
    3. TRASH STATUS: Results of the trash emptying operation including number of messages permanently removed
    The report should be in JSON format for easy parsing and tracking.
  agent: cleaner
  context: [categorization_task, organization_task]
  output_file: output/cleanup_report.json


================================================
FILE: src/gmail_crew_ai/crew.py
================================================
from crewai import Agent, Crew, Process, Task, LLM
from crewai.project import CrewBase, agent, crew, task, before_kickoff
from crewai_tools import FileReadTool
import json
import os
from typing import List, Dict, Any, Callable
from pydantic import SkipValidation
from datetime import date, datetime

from gmail_crew_ai.tools.gmail_tools import GetUnreadEmailsTool, SaveDraftTool, GmailOrganizeTool, GmailDeleteTool, EmptyTrashTool
from gmail_crew_ai.tools.slack_tool import SlackNotificationTool
from gmail_crew_ai.tools.date_tools import DateCalculationTool
from gmail_crew_ai.models import CategorizedEmail, OrganizedEmail, EmailResponse, SlackNotification, EmailCleanupInfo, SimpleCategorizedEmail, EmailDetails

@CrewBase
class GmailCrewAi():
	"""Crew that processes emails."""
	agents_config = 'config/agents.yaml'
	tasks_config = 'config/tasks.yaml'

	@before_kickoff
	def fetch_emails(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
		"""Fetch emails before starting the crew and calculate ages."""
		print("Fetching emails before starting the crew...")
		
		# Get the email limit from inputs
		email_limit = inputs.get('email_limit', 5)
		print(f"Fetching {email_limit} emails...")
		
		# Create the output directory if it doesn't exist
		os.makedirs("output", exist_ok=True)
		
		# Use the GetUnreadEmailsTool directly
		email_tool = GetUnreadEmailsTool()
		email_tuples = email_tool._run(limit=email_limit)
		
		# Convert email tuples to EmailDetails objects with pre-calculated ages
		emails = []
		today = date.today()
		for email_tuple in email_tuples:
			email_detail = EmailDetails.from_email_tuple(email_tuple)
			
			# Calculate age if date is available
			if email_detail.date:
				try:
					email_date_obj = datetime.strptime(email_detail.date, "%Y-%m-%d").date()
					email_detail.age_days = (today - email_date_obj).days
					print(f"Email date: {email_detail.date}, age: {email_detail.age_days} days")
				except Exception as e:
					print(f"Error calculating age for email date {email_detail.date}: {e}")
					email_detail.age_days = None
			
			emails.append(email_detail.dict())
		
		# Save emails to file
		with open('output/fetched_emails.json', 'w') as f:
			json.dump(emails, f, indent=2)
		
		print(f"Fetched and saved {len(emails)} emails to output/fetched_emails.json")
		
		return inputs
	
	llm = LLM(
		model="openai/gpt-4o-mini",
		api_key=os.getenv("OPENAI_API_KEY"),
	)

	@agent
	def categorizer(self) -> Agent:
		"""The email categorizer agent."""
		return Agent(
			config=self.agents_config['categorizer'],
			tools=[FileReadTool()],
			llm=self.llm,
		)

	@agent
	def organizer(self) -> Agent:
		"""The email organization agent."""
		return Agent(
			config=self.agents_config['organizer'],
			tools=[GmailOrganizeTool(), FileReadTool()],
			llm=self.llm,
		)
		
	@agent
	def response_generator(self) -> Agent:
		"""The email response generator agent."""
		return Agent(
			config=self.agents_config['response_generator'],
			tools=[SaveDraftTool()],
			llm=self.llm,
		)
	
	@agent
	def notifier(self) -> Agent:
		"""The email notification agent."""
		return Agent(
			config=self.agents_config['notifier'],
			tools=[SlackNotificationTool()],
			llm=self.llm,
		)

	@agent
	def cleaner(self) -> Agent:
		"""The email cleanup agent."""
		return Agent(
			config=self.agents_config['cleaner'],
			tools=[GmailDeleteTool(), EmptyTrashTool()],
			llm=self.llm,
		)

	@task
	def categorization_task(self) -> Task:
		"""The email categorization task."""
		return Task(
			config=self.tasks_config['categorization_task'],
			output_pydantic=SimpleCategorizedEmail
		)
	
	@task
	def organization_task(self) -> Task:
		"""The email organization task."""
		return Task(
			config=self.tasks_config['organization_task'],
			output_pydantic=OrganizedEmail,
		)

	@task
	def response_task(self) -> Task:
		"""The email response task."""
		return Task(
			config=self.tasks_config['response_task'],
			output_pydantic=EmailResponse,
		)
	
	@task
	def notification_task(self) -> Task:
		"""The email notification task."""
		return Task(
			config=self.tasks_config['notification_task'],
			output_pydantic=SlackNotification,
		)

	@task
	def cleanup_task(self) -> Task:
		"""The email cleanup task."""
		return Task(
			config=self.tasks_config['cleanup_task'],
			output_pydantic=EmailCleanupInfo,
		)

	@crew
	def crew(self) -> Crew:
		"""Creates the email processing crew."""
		return Crew(
			agents=self.agents,
			tasks=self.tasks,
			process=Process.sequential,
			verbose=True
		)

	def _debug_callback(self, event_type, payload):
		"""Debug callback for crew events."""
		if event_type == "task_start":
			print(f"DEBUG: Starting task: {payload.get('task_name')}")
		elif event_type == "task_end":
			print(f"DEBUG: Finished task: {payload.get('task_name')}")
			print(f"DEBUG: Task output type: {type(payload.get('output'))}")
			
			# Add more detailed output inspection
			output = payload.get('output')
			if output:
				if isinstance(output, dict):
					print(f"DEBUG: Output keys: {output.keys()}")
					for key, value in output.items():
						print(f"DEBUG: {key}: {value[:100] if isinstance(value, str) and len(value) > 100 else value}")
				elif isinstance(output, list):
					print(f"DEBUG: Output list length: {len(output)}")
					if output and len(output) > 0:
						print(f"DEBUG: First item type: {type(output[0])}")
						if isinstance(output[0], dict):
							print(f"DEBUG: First item keys: {output[0].keys()}")
				else:
					print(f"DEBUG: Output: {str(output)[:200]}...")
		elif event_type == "agent_start":
			print(f"DEBUG: Agent starting: {payload.get('agent_name')}")
		elif event_type == "agent_end":
			print(f"DEBUG: Agent finished: {payload.get('agent_name')}")
		elif event_type == "error":
			print(f"DEBUG: Error: {payload.get('error')}")

	def _validate_categorization_output(self, output):
		"""Validate the categorization output before writing to file."""
		print(f"DEBUG: Validating categorization output: {output}")
		
		# If output is empty or invalid, provide a default
		if not output:
			print("WARNING: Empty categorization output, providing default")
			return {
				"email_id": "",
				"subject": "",
				"category": "",
				"priority": "",
				"required_action": ""
			}
		
		# If output is a string (which might happen if the LLM returns JSON as a string)
		if isinstance(output, str):
			try:
				# Try to parse it as JSON
				import json
				# First, check if the string starts with "my best complete final answer"
				if "my best complete final answer" in output.lower():
					# Extract the JSON part
					json_start = output.find("{")
					json_end = output.rfind("}") + 1
					if json_start >= 0 and json_end > json_start:
						json_str = output[json_start:json_end]
						parsed = json.loads(json_str)
						print("DEBUG: Successfully extracted and parsed JSON from answer")
						return parsed
				
				# Try to parse the whole string as JSON
				parsed = json.loads(output)
				print("DEBUG: Successfully parsed string output as JSON")
				return parsed
			except Exception as e:
				print(f"WARNING: Output is a string but not valid JSON: {e}")
				# Try to extract anything that looks like JSON
				import re
				json_pattern = r'\{.*\}'
				match = re.search(json_pattern, output, re.DOTALL)
				if match:
					try:
						json_str = match.group(0)
						parsed = json.loads(json_str)
						print("DEBUG: Successfully extracted and parsed JSON using regex")
						return parsed
					except:
						print("WARNING: Failed to parse extracted JSON")
		
		# If output is already a dict, make sure it has the required fields
		if isinstance(output, dict):
			required_fields = ["email_id", "subject", "category", "priority", "required_action"]
			missing_fields = [field for field in required_fields if field not in output]
			
			if missing_fields:
				print(f"WARNING: Output missing required fields: {missing_fields}")
				# Add missing fields with empty values
				for field in missing_fields:
					output[field] = ""
			
			# Check if the values match the expected format
			if output.get("email_id") == "12345" and output.get("subject") == "Urgent Task Update":
				print("WARNING: Output contains placeholder values, trying to fix")
				# Try to get the real email ID from the fetched emails
				try:
					with open("output/fetched_emails.json", "r") as f:
						fetched_emails = json.load(f)
						if fetched_emails and len(fetched_emails) > 0:
							real_email = fetched_emails[0]
							output["email_id"] = real_email.get("email_id", "")
							output["subject"] = real_email.get("subject", "")
				except Exception as e:
					print(f"WARNING: Failed to fix placeholder values: {e}")
		
		return output


================================================
FILE: src/gmail_crew_ai/main.py
================================================
#!/usr/bin/env python
import sys
import warnings
from dotenv import load_dotenv

# Remove or comment out these debug lines
# import litellm
# litellm._turn_on_debug()

# Add this line to suppress the warning
warnings.filterwarnings("ignore", message=".*not a Python type.*")
# Keep your existing warning filter
warnings.filterwarnings("ignore", category=SyntaxWarning, module="pysbd")

from gmail_crew_ai.crew import GmailCrewAi

def run():
    """Run the Gmail Crew AI."""
    try:
        # Load environment variables
        load_dotenv()
        
        # Get user input for number of emails to process
        try:
            email_limit = input("How many emails would you like to process? (default: 5): ")
            if email_limit.strip() == "":
                email_limit = 5
            else:
                email_limit = int(email_limit)
                if email_limit <= 0:
                    print("Number must be positive. Using default of 5.")
                    email_limit = 5
        except ValueError:
            print("Invalid input. Using default of 5 emails.")
            email_limit = 5
        
        print(f"Processing {email_limit} emails...")
        
        # Create and run the crew with the specified email limit
        result = GmailCrewAi().crew().kickoff(inputs={'email_limit': email_limit})
        
        # Check if result is empty or None
        if not result:
            print("\nNo emails were processed. Inbox might be empty.")
            return 0
            
        # Print the result in a clean way
        if result:
            print("\nCrew execution completed successfully! 🎉")
            print("Results have been saved to the output directory.")
            return 0  # Return success code
        else:
            print("\nCrew execution completed but no results were returned.")
            return 0  # Still consider this a success
    except Exception as e:
        print(f"\nError: {e}")
        return 1  # Return error code

if __name__ == "__main__":
    sys.exit(run())  # Use the return value as the exit code


================================================
FILE: src/gmail_crew_ai/models.py
================================================
from pydantic import BaseModel, Field, SkipValidation
from typing import List, Optional, Dict, Literal, Callable, Any
from datetime import datetime

class EmailDetails(BaseModel):
    """Model for email details."""
    email_id: Optional[str] = Field(None, description="Email ID")
    subject: Optional[str] = Field(None, description="Email subject")
    sender: Optional[str] = Field(None, description="Email sender")
    body: Optional[str] = Field(None, description="Email body")
    date: Optional[str] = Field(None, description="Email date (YYYY-MM-DD)")
    age_days: Optional[int] = Field(None, description="Age of the email in days from today")
    thread_info: Optional[Dict[str, Any]] = Field(None, description="Thread information")
    is_part_of_thread: Optional[bool] = Field(False, description="Whether this email is part of a thread")
    thread_size: Optional[int] = Field(1, description="Number of emails in this thread")
    thread_position: Optional[int] = Field(1, description="Position of this email in the thread (1 = first)")

    @classmethod
    def from_email_tuple(cls, email_tuple):
        """Create an EmailDetails from an email tuple."""
        if not email_tuple or len(email_tuple) < 5:
            return cls(email_id=None, subject=None)
        
        subject, sender, body, email_id, thread_info = email_tuple
        
        # Extract date from thread_info
        date = ""
        if isinstance(thread_info, dict) and 'date' in thread_info:
            date = thread_info['date']
            
        return cls(
            email_id=email_id,
            subject=subject,
            sender=sender,
            body=body,
            date=date,
            thread_info=thread_info
        )

# Define the valid categories, priorities, and actions as type aliases
EmailCategoryType = Literal["NEWSLETTERS", "PROMOTIONS", "PERSONAL", "GITHUB", 
                           "SPONSORSHIPS", "RECRUITMENT", "COLD_EMAIL", 
                           "EVENT_INVITATIONS", "RECEIPTS_INVOICES", "YOUTUBE", "SOCIALS"]

EmailPriorityType = Literal["HIGH", "MEDIUM", "LOW"]

EmailActionType = Literal["REPLY", "READ_ONLY", "TASK", "IGNORE"]

class CategorizedEmail(BaseModel):
    """Model for categorized email information."""
    email_id: str = Field(..., description="Unique identifier for the email")
    subject: str = Field(..., description="Email subject line")
    sender: str = Field(..., description="Email sender (name and address)")
    date: str = Field(..., description="Email date in YYYY-MM-DD format")
    category: EmailCategoryType = Field(..., description="Category of the email")
    priority: EmailPriorityType = Field(..., description="Priority level of the email")
    required_action: EmailActionType = Field(..., description="Required action for the email")
    reason: str = Field(..., description="Reason for the categorization")
    due_date: Optional[str] = Field(None, description="Due date for action if applicable")
    thread_info: Optional[Dict] = Field(default=None, description="Thread information for replies")

class OrganizedEmail(BaseModel):
    """Model for organized email information."""
    email_id: str = Field(..., description="Unique identifier for the email")
    subject: str = Field(..., description="Email subject line")
    applied_labels: List[str] = Field(default_factory=list, description="Labels applied to the email")
    starred: bool = Field(default=False, description="Whether the email was starred")
    result: str = Field(..., description="Result of the organization attempt")

class EmailResponse(BaseModel):
    """Model for email response information."""
    email_id: str = Field(..., description="Unique identifier for the email")
    subject: str = Field(..., description="Email subject line")
    recipient: str = Field(..., description="Email recipient")
    response_summary: str = Field(..., description="Summary of the response")
    response_needed: bool = Field(..., description="Whether a response was needed")
    draft_saved: bool = Field(default=False, description="Whether a draft was saved")

class SlackNotification(BaseModel):
    """Model for Slack notification information."""
    email_id: str = Field(..., description="Unique identifier for the email")
    subject: str = Field(..., description="Email subject line")
    sender: str = Field(..., description="Email sender")
    category: EmailCategoryType = Field(..., description="Category of the email")
    priority: EmailPriorityType = Field(..., description="Priority level of the email")
    summary: str = Field(..., description="Brief summary of the email content")
    action_needed: Optional[str] = Field(None, description="Action needed, if any")
    headline: str = Field(..., description="Custom headline for the notification")
    intro: str = Field(..., description="Custom intro phrase for the notification")
    action_header: Optional[str] = Field(None, description="Custom header for the action section")
    notification_sent: bool = Field(default=False, description="Whether notification was sent")

class EmailCleanupInfo(BaseModel):
    """Model for email cleanup information."""
    email_id: str = Field(..., description="Unique identifier for the email")
    subject: str = Field(..., description="Email subject line")
    sender: str = Field(..., description="Email sender")
    age_days: int = Field(..., description="Age of the email in days")
    deleted: bool = Field(..., description="Whether the email was deleted")
    reason: str = Field(..., description="Reason for deletion or preservation")

class SimpleCategorizedEmail(BaseModel):
    """Simplified model for debugging."""
    email_id: Optional[str] = Field(None, description="Unique identifier for the email")
    subject: Optional[str] = Field(None, description="Email subject line")
    sender: Optional[str] = Field(None, description="Email sender address")
    category: Optional[str] = Field(None, description="Category of the email")
    priority: Optional[str] = Field(None, description="Priority level of the email")
    required_action: Optional[str] = Field(None, description="Required action for the email")
    date: Optional[str] = Field(None, description="Date the email was received (YYYY-MM-DD)")
    age_days: Optional[int] = Field(None, description="Age of the email in days from today")

    # Update the from_email_tuple method to include date
    @classmethod
    def from_email_tuple(cls, email_tuple):
        """Create a SimpleCategorizedEmail from an email tuple."""
        if not email_tuple or len(email_tuple) < 5:
            return cls(email_id=None, subject=None)
        
        subject, sender, body, email_id, thread_info = email_tuple
        
        # Extract date from body or thread_info
        date = ""
        if isinstance(thread_info, dict) and 'date' in thread_info:
            date = thread_info['date']
        elif body and body.startswith("EMAIL DATE:"):
            date_line = body.split("\n")[0]
            date = date_line.replace("EMAIL DATE:", "").strip()
        
        return cls(
            email_id=email_id,
            subject=subject,
            sender=sender,
            date=date
        )


================================================
FILE: src/gmail_crew_ai/tools/__init__.py
================================================


================================================
FILE: src/gmail_crew_ai/tools/date_tools.py
================================================
from datetime import datetime, date
from typing import Optional
from pydantic import BaseModel, Field
from crewai.tools import BaseTool
import time

class DateCalculationSchema(BaseModel):
    """Schema for DateCalculationTool input."""
    email_date: str = Field(..., description="Email date in ISO format (YYYY-MM-DD)")
    # Remove reference_date from schema to prevent agent from using it
    # reference_date: Optional[str] = Field(None, description="Reference date in ISO format (YYYY-MM-DD). Defaults to today.")

class DateCalculationTool(BaseTool):
    """Tool to calculate the age of an email in days."""
    name: str = "calculate_email_age"
    description: str = "Calculate how many days old an email is compared to today's date"
    args_schema: type[BaseModel] = DateCalculationSchema

    def _run(self, email_date: str, reference_date: Optional[str] = None) -> str:
        """Calculate the age of an email in days compared to today.
        
        Args:
            email_date: The date of the email in YYYY-MM-DD format
            
        Returns:
            A string with the email age information
        """
        try:
            # Parse the email date
            email_date_obj = datetime.strptime(email_date, "%Y-%m-%d").date()
            
            # Always use today's date - ignore any provided reference_date
            today = date.today()
            
            # Calculate the age in days
            age_days = (today - email_date_obj).days
            
            # Create the response
            response = f"Email age: {age_days} days from today ({today})\n"
            response += f"Email date: {email_date_obj}\n"
            response += f"- Less than 5 days old: {'Yes' if age_days < 5 else 'No'}\n"
            response += f"- Older than 7 days: {'Yes' if age_days > 7 else 'No'}\n"
            response += f"- Older than 10 days: {'Yes' if age_days > 10 else 'No'}\n"
            response += f"- Older than 14 days: {'Yes' if age_days > 14 else 'No'}\n"
            response += f"- Older than 30 days: {'Yes' if age_days > 30 else 'No'}\n"
            
            return response
            
        except Exception as e:
            return f"Error calculating email age: {str(e)}" 

================================================
FILE: src/gmail_crew_ai/tools/gmail_tools.py
================================================
import imaplib
import email
from email.header import decode_header
from typing import List, Tuple, Literal, Optional, Type, Dict, Any
import re
from bs4 import BeautifulSoup
from crewai.tools import BaseTool
import os
from pydantic import BaseModel, Field
from crewai.tools import tool
import time
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import base64

def decode_header_safe(header):
    """
    Safely decode email headers that might contain encoded words or non-ASCII characters.
    """
    if not header:
        return ""
    
    try:
        decoded_parts = []
        for decoded_str, charset in decode_header(header):
            if isinstance(decoded_str, bytes):
                if charset:
                    decoded_parts.append(decoded_str.decode(charset or 'utf-8', errors='replace'))
                else:
                    decoded_parts.append(decoded_str.decode('utf-8', errors='replace'))
            else:
                decoded_parts.append(str(decoded_str))
        return ' '.join(decoded_parts)
    except Exception as e:
        # Fallback to raw header if decoding fails
        return str(header)

def clean_email_body(email_body: str) -> str:
    """
    Clean the email body by removing HTML tags and excessive whitespace.
    """
    try:
        soup = BeautifulSoup(email_body, "html.parser")
        text = soup.get_text(separator=" ")  # Get text with spaces instead of <br/>
    except Exception as e:
        print(f"Error parsing HTML: {e}")
        text = email_body  # Fallback to raw body if parsing fails

    # Remove excessive whitespace and newlines
    text = re.sub(r'\s+', ' ', text).strip()
    return text

class GmailToolBase(BaseTool):
    """Base class for Gmail tools, handling connection and credentials."""
    
    class Config:
        arbitrary_types_allowed = True

    email_address: Optional[str] = Field(None, description="Gmail email address")
    app_password: Optional[str] = Field(None, description="Gmail app password")

    def __init__(self, description: str = ""):
        super().__init__(description=description)
        self.email_address = os.environ.get("EMAIL_ADDRESS")
        self.app_password = os.environ.get("APP_PASSWORD")

        if not self.email_address or not self.app_password:
            raise ValueError("EMAIL_ADDRESS and APP_PASSWORD must be set in the environment.")

    def _connect(self):
        """Connect to Gmail."""
        try:
            print(f"Connecting to Gmail with email: {self.email_address[:3]}...{self.email_address[-8:]}")
            mail = imaplib.IMAP4_SSL("imap.gmail.com")
            mail.login(self.email_address, self.app_password)
            print("Successfully logged in to Gmail")
            return mail
        except Exception as e:
            print(f"Error connecting to Gmail: {e}")
            raise e

    def _disconnect(self, mail):
        """Disconnect from Gmail."""
        try:
            mail.close()
            mail.logout()
            print("Successfully disconnected from Gmail")
        except:
            pass

    def _get_thread_messages(self, mail: imaplib.IMAP4_SSL, msg) -> List[str]:
        """Get all messages in the thread by following References and In-Reply-To headers."""
        thread_messages = []
        
        # Get message IDs from References and In-Reply-To headers
        references = msg.get("References", "").split()
        in_reply_to = msg.get("In-Reply-To", "").split()
        message_ids = list(set(references + in_reply_to))  # Remove duplicates
        
        if message_ids:
            # Search for messages with these Message-IDs
            search_criteria = ' OR '.join(f'HEADER MESSAGE-ID "{mid}"' for mid in message_ids)
            result, data = mail.search(None, search_criteria)
            
            if result == "OK":
                thread_ids = data[0].split()
                for thread_id in thread_ids:
                    result, msg_data = mail.fetch(thread_id, "(RFC822)")
                    if result == "OK":
                        thread_msg = email.message_from_bytes(msg_data[0][1])
                        # Extract body from thread message
                        thread_body = self._extract_body(thread_msg)
                        thread_messages.append(thread_body)
        
        return thread_messages

    def _extract_body(self, msg) -> str:
        """Extract body from an email message."""
        body = ""
        if msg.is_multipart():
            for part in msg.walk():
                content_type = part.get_content_type()
                content_disposition = str(part.get("Content-Disposition"))

                try:
                    email_body = part.get_payload(decode=True).decode()
                except:
                    email_body = ""

                if content_type == "text/plain" and "attachment" not in content_disposition:
                    body += email_body
                elif content_type == "text/html" and "attachment" not in content_disposition:
                    body += clean_email_body(email_body)
        else:
            try:
                body = clean_email_body(msg.get_payload(decode=True).decode())
            except Exception as e:
                body = f"Error decoding body: {e}"
        return body

class GetUnreadEmailsSchema(BaseModel):
    """Schema for GetUnreadEmailsTool input."""
    limit: Optional[int] = Field(
        default=5,
        description="Maximum number of unread emails to retrieve. Defaults to 5.",
        ge=1  # Ensures the limit is greater than or equal to 1
    )

class GetUnreadEmailsTool(GmailToolBase):
    """Tool to get unread emails from Gmail."""
    name: str = "get_unread_emails"
    description: str = "Gets unread emails from Gmail"
    args_schema: Type[BaseModel] = GetUnreadEmailsSchema
    
    def _run(self, limit: Optional[int] = 5) -> List[Tuple[str, str, str, str, Dict]]:
        mail = self._connect()
        try:
            print("DEBUG: Connecting to Gmail...")
            mail.select("INBOX")
            result, data = mail.search(None, 'UNSEEN')
            
            print(f"DEBUG: Search result: {result}")
            
            if result != "OK":
                print("DEBUG: Error searching for unseen emails")
                return []
            
            email_ids = data[0].split()
            print(f"DEBUG: Found {len(email_ids)} unread emails")
            
            if not email_ids:
                print("DEBUG: No unread emails found.")
                return []
            
            email_ids = list(reversed(email_ids))
            email_ids = email_ids[:limit]
            print(f"DEBUG: Processing {len(email_ids)} emails")
            
            emails = []
            for i, email_id in enumerate(email_ids):
                print(f"DEBUG: Processing email {i+1}/{len(email_ids)}")
                result, msg_data = mail.fetch(email_id, "(RFC822)")
                if result != "OK":
                    print(f"Error fetching email {email_id}:", result)
                    continue

                raw_email = msg_data[0][1]
                msg = email.message_from_bytes(raw_email)

                # Decode headers properly (handles encoded characters)
                subject = decode_header_safe(msg["Subject"])
                sender = decode_header_safe(msg["From"])
                
                # Extract and standardize the date
                date_str = msg.get("Date", "")
                received_date = self._parse_email_date(date_str)
                
                # Get the current message body
                current_body = self._extract_body(msg)
                
                # Get thread messages
                thread_messages = self._get_thread_messages(mail, msg)
                
                # Combine current message with thread history
                full_body = "\n\n--- Previous Messages ---\n".join([current_body] + thread_messages)

                # Get thread metadata
                thread_info = {
                    'message_id': msg.get('Message-ID', ''),
                    'in_reply_to': msg.get('In-Reply-To', ''),
                    'references': msg.get('References', ''),
                    'date': received_date,  # Use standardized date
                    'raw_date': date_str,   # Keep original date string
                    'email_id': email_id.decode('utf-8')
                }

                # Add a clear date indicator in the body for easier extraction
                full_body = f"EMAIL DATE: {received_date}\n\n{full_body}"
                
                # Print the structure of what we're appending
                print(f"DEBUG: Email tuple structure: subject={subject}, sender={sender}, body_length={len(full_body)}, email_id={email_id.decode('utf-8')}, thread_info_keys={thread_info.keys()}")
                
                emails.append((subject, sender, full_body, email_id.decode('utf-8'), thread_info))
            
            print(f"DEBUG: Returning {len(emails)} email tuples")
            return emails
        except Exception as e:
            print(f"DEBUG: Exception in GetUnreadEmailsTool: {e}")
            import traceback
            traceback.print_exc()
            return []
        finally:
            self._disconnect(mail)

    def _parse_email_date(self, date_str: str) -> str:
        """
        Parse email date string into a standardized format.
        Returns ISO format date string (YYYY-MM-DD) or empty string if parsing fails.
        """
        if not date_str:
            return ""
        
        try:
            # Try various date formats commonly found in emails
            # Remove timezone name if present (like 'EDT', 'PST')
            date_str = re.sub(r'\s+\([A-Z]{3,4}\)', '', date_str)
            
            # Parse with email.utils
            parsed_date = email.utils.parsedate_to_datetime(date_str)
            if parsed_date:
                return parsed_date.strftime("%Y-%m-%d")
        except Exception as e:
            print(f"Error parsing date '{date_str}': {e}")
        
        return ""

class SaveDraftSchema(BaseModel):
    """Schema for SaveDraftTool input."""
    subject: str = Field(..., description="Email subject")
    body: str = Field(..., description="Email body content")
    recipient: str = Field(..., description="Recipient email address")
    thread_info: Optional[Dict[str, Any]] = Field(None, description="Thread information for replies")

class SaveDraftTool(BaseTool):
    """Tool to save an email as a draft using IMAP."""
    name: str = "save_email_draft"
    description: str = "Saves an email as a draft in Gmail"
    args_schema: Type[BaseModel] = SaveDraftSchema

    def _format_body(self, body: str) -> str:
        """Format the email body with signature."""
        # Replace [Your name] or [Your Name] with Tony Kipkemboi
        body = re.sub(r'\[Your [Nn]ame\]', 'Tony Kipkemboi', body)
        
        # If no placeholder was found, append the signature
        if '[Your' not in body and '[your' not in body:
            body = f"{body}\n\nBest regards,\nTony Kipkemboi"
        
        return body

    def _connect(self):
        """Connect to Gmail using IMAP."""
        # Get email credentials from environment
        email_address = os.environ.get('EMAIL_ADDRESS')
        app_password = os.environ.get('APP_PASSWORD')
        
        if not email_address or not app_password:
            raise ValueError("EMAIL_ADDRESS or APP_PASSWORD environment variables not set")
        
        # Connect to Gmail's IMAP server
        mail = imaplib.IMAP4_SSL('imap.gmail.com')
        print(f"Connecting to Gmail with email: {email_address[:3]}...{email_address[-10:]}")
        mail.login(email_address, app_password)
        return mail, email_address

    def _disconnect(self, mail):
        """Disconnect from Gmail."""
        try:
            mail.logout()
        except:
            pass

    def _check_drafts_folder(self, mail):
        """Check available mailboxes to find the drafts folder."""
        print("Checking available mailboxes...")
        result, mailboxes = mail.list()
        if result == 'OK':
            drafts_folders = []
            for mailbox in mailboxes:
                if b'Drafts' in mailbox or b'Draft' in mailbox:
                    drafts_folders.append(mailbox.decode())
                    print(f"Found drafts folder: {mailbox.decode()}")
            return drafts_folders
        return []

    def _verify_draft_saved(self, mail, subject, recipient):
        """Verify if the draft was actually saved by searching for it."""
        try:
            # Try different drafts folder names
            drafts_folders = [
                '"[Gmail]/Drafts"', 
                'Drafts',
                'DRAFTS',
                '"[Google Mail]/Drafts"',
                '[Gmail]/Drafts'
            ]
            
            for folder in drafts_folders:
                try:
                    print(f"Checking folder: {folder}")
                    result, _ = mail.select(folder, readonly=True)
                    if result != 'OK':
                        continue
                        
                    # Search for drafts with this subject
                    search_criteria = f'SUBJECT "{subject}"'
                    result, data = mail.search(None, search_criteria)
                    
                    if result == 'OK' and data[0]:
                        draft_count = len(data[0].split())
                        print(f"Found {draft_count} drafts matching subject '{subject}' in folder {folder}")
                        return True, folder
                    else:
                        print(f"No drafts found matching subject '{subject}' in folder {folder}")
                except Exception as e:
                    print(f"Error checking folder {folder}: {e}")
                    continue
                    
            return False, None
        except Exception as e:
            print(f"Error verifying draft: {e}")
            return False, None

    def _run(self, subject: str, body: str, recipient: str, thread_info: Optional[Dict[str, Any]] = None) -> str:
        try:
            mail, email_address = self._connect()
            
            # Check available drafts folders
            drafts_folders = self._check_drafts_folder(mail)
            print(f"Available drafts folders: {drafts_folders}")
            
            # Try with quoted folder name first
            drafts_folder = '"[Gmail]/Drafts"'
            print(f"Selecting drafts folder: {drafts_folder}")
            result, _ = mail.select(drafts_folder)
            
            # If that fails, try without quotes
            if result != 'OK':
                drafts_folder = '[Gmail]/Drafts'
                print(f"First attempt failed. Trying: {drafts_folder}")
                result, _ = mail.select(drafts_folder)
                
            # If that also fails, try just 'Drafts'
            if result != 'OK':
                drafts_folder = 'Drafts'
                print(f"Second attempt failed. Trying: {drafts_folder}")
                result, _ = mail.select(drafts_folder)
                
            if result != 'OK':
                return f"Error: Could not select drafts folder. Available folders: {drafts_folders}"
                
            print(f"Successfully selected drafts folder: {drafts_folder}")
            
            # Format body and add signature
            body_with_signature = self._format_body(body)
            
            # Create the email message
            message = email.message.EmailMessage()
            message["From"] = email_address
            message["To"] = recipient
            message["Subject"] = subject
            message.set_content(body_with_signature)
            print(f"Created message with subject: {subject}")

            # Add thread headers if this is a reply
            if thread_info:
                # References header should include all previous message IDs
                references = []
                if thread_info.get('references'):
                    references.extend(thread_info['references'].split())
                if thread_info.get('message_id'):
                    references.append(thread_info['message_id'])
                
                if references:
                    message["References"] = " ".join(references)
                
                # In-Reply-To should point to the immediate parent message
                if thread_info.get('message_id'):
                    message["In-Reply-To"] = thread_info['message_id']

                # Make sure subject has "Re: " prefix
                if not subject.lower().startswith('re:'):
                    message["Subject"] = f"Re: {subject}"
                    
                print(f"Added thread information for reply")

            # Save to drafts
            print(f"Attempting to save draft to {drafts_folder}...")
            date = imaplib.Time2Internaldate(time.time())
            result, data = mail.append(drafts_folder, '\\Draft', date, message.as_bytes())
            
            if result != 'OK':
                return f"Error saving draft: {result}, {data}"
                
            print(f"Draft save attempt result: {result}")
            
            # Verify the draft was actually saved
            verified, folder = self._verify_draft_saved(mail, subject, recipient)
            
            if verified:
                return f"VERIFIED: Draft email saved with subject: '{subject}' in folder {folder}"
            else:
                # Try Gmail's API approach as a fallback
                try:
                    # Try saving directly to All Mail and flagging as draft
                    result, data = mail.append('[Gmail]/All Mail', '\\Draft', date, message.as_bytes())
                    if result == 'OK':
                        return f"Draft saved to All Mail with subject: '{subject}' (flagged as draft)"
                    else:
                        return f"WARNING: Draft save attempt returned {result}, but verification failed. Please check your Gmail Drafts folder."
                except Exception as e:
                    return f"WARNING: Draft may not have been saved properly: {str(e)}"

        except Exception as e:
            return f"Error saving draft: {str(e)}"
        finally:
            self._disconnect(mail)

class GmailOrganizeSchema(BaseModel):
    """Schema for GmailOrganizeTool input."""
    email_id: str = Field(..., description="Email ID to organize")
    category: str = Field(..., description="Category assigned by agent (Urgent/Response Needed/etc)")
    priority: str = Field(..., description="Priority level (High/Medium/Low)")
    should_star: bool = Field(default=False, description="Whether to star the email")
    labels: List[str] = Field(default_list=[], description="Labels to apply")

class GmailOrganizeTool(GmailToolBase):
    """Tool to organize emails based on agent categorization."""
    name: str = "organize_email"
    description: str = "Organizes emails using Gmail's priority features based on category and priority"
    args_schema: Type[BaseModel] = GmailOrganizeSchema

    def _run(self, email_id: str, category: str, priority: str, should_star: bool = False, labels: List[str] = None) -> str:
        """Organize an email with the specified parameters."""
        if labels is None:
            # Provide a default empty list to avoid validation errors
            labels = []
        
        print(f"Organizing email {email_id} with category {category}, priority {priority}, star={should_star}, labels={labels}")
        
        mail = self._connect()
        try:
            # Select inbox to ensure we can access the email
            mail.select("INBOX")
            
            # Apply organization based on category and priority
            if category == "Urgent Response Needed" and priority == "High":
                # Star the email
                if should_star:
                    mail.store(email_id, '+FLAGS', '\\Flagged')
                
                # Mark as important
                mail.store(email_id, '+FLAGS', '\\Important')
                
                # Apply URGENT label if it doesn't exist
                if "URGENT" not in labels:
                    labels.append("URGENT")

            # Apply all specified labels
            for label in labels:
                try:
                    # Create label if it doesn't exist
                    mail.create(label)
                except:
                    pass  # Label might already exist
                
                # Apply label
                mail.store(email_id, '+X-GM-LABELS', label)

            return f"Email organized: Starred={should_star}, Labels={labels}"

        except Exception as e:
            return f"Error organizing email: {e}"
        finally:
            self._disconnect(mail)

class GmailDeleteSchema(BaseModel):
    """Schema for GmailDeleteTool input."""
    email_id: str = Field(..., description="Email ID to delete")
    reason: str = Field(..., description="Reason for deletion")

class GmailDeleteTool(BaseTool):
    """Tool to delete an email using IMAP."""
    name: str = "delete_email"
    description: str = "Deletes an email from Gmail"
    
    def _run(self, email_id: str, reason: str) -> str:
        """
        Delete an email by ID.
        Parameters:
            email_id: The email ID to delete
            reason: The reason for deletion (for logging)
        """
        try:
            # Validate inputs - Add this validation
            if not email_id or not isinstance(email_id, str):
                return f"Error: Invalid email_id format: {email_id}"
            
            if not reason or not isinstance(reason, str):
                return f"Error: Invalid reason format: {reason}"
                
            mail = self._connect()
            try:
                mail.select("INBOX")
                
                # First verify the email exists and get its details for logging
                result, data = mail.fetch(email_id, "(RFC822)")
                if result != "OK" or not data or data[0] is None:
                    return f"Error: Email with ID {email_id} not found"
                    
                msg = email.message_from_bytes(data[0][1])
                subject = decode_header_safe(msg["Subject"])
                sender = decode_header_safe(msg["From"])
                
                # Move to Trash
                mail.store(email_id, '+X-GM-LABELS', '\\Trash')
                mail.store(email_id, '-X-GM-LABELS', '\\Inbox')
                
                return f"Email deleted: '{subject}' from {sender}. Reason: {reason}"
            except Exception as e:
                return f"Error deleting email: {e}"
            finally:
                self._disconnect(mail)

        except Exception as e:
            return f"Error deleting email: {str(e)}"

class EmptyTrashTool(BaseTool):
    """Tool to empty Gmail trash."""
    name: str = "empty_gmail_trash"
    description: str = "Empties the Gmail trash folder to free up space"

    def _connect(self):
        """Connect to Gmail using IMAP."""
        # Get email credentials from environment
        email_address = os.environ.get('EMAIL_ADDRESS')
        app_password = os.environ.get('APP_PASSWORD')
        
        if not email_address or not app_password:
            raise ValueError("EMAIL_ADDRESS or APP_PASSWORD environment variables not set")
        
        # Connect to Gmail's IMAP server
        mail = imaplib.IMAP4_SSL('imap.gmail.com')
        print(f"Connecting to Gmail with email: {email_address[:3]}...{email_address[-10:]}")
        mail.login(email_address, app_password)
        return mail

    def _disconnect(self, mail):
        """Disconnect from Gmail."""
        try:
            mail.logout()
        except:
            pass
    
    def _run(self) -> str:
        """Empty the Gmail trash folder."""
        try:
            mail = self._connect()
            
            # Try different trash folder names (Gmail can have different naming conventions)
            trash_folders = [
                '"[Gmail]/Trash"',
                '[Gmail]/Trash',
                'Trash',
                '"[Google Mail]/Trash"',
                '[Google Mail]/Trash'
            ]
            
            success = False
            trash_folder_used = None
            
            for folder in trash_folders:
                try:
                    print(f"Attempting to select trash folder: {folder}")
                    result, data = mail.select(folder)
                    
                    if result == 'OK':
                        trash_folder_used = folder
                        print(f"Successfully selected trash folder: {folder}")
                        
                        # Search for all messages in trash
                        result, data = mail.search(None, 'ALL')
                        
                        if result == 'OK':
                            email_ids = data[0].split()
                            count = len(email_ids)
                            
                            if count == 0:
                                print("No messages found in trash.")
                                return "Trash is already empty. No messages to delete."
                            
                            print(f"Found {count} messages in trash.")
                            
                            # Delete all messages in trash
                            for email_id in email_ids:
                                mail.store(email_id, '+FLAGS', '\\Deleted')
                            
                            # Permanently remove messages marked for deletion
                            mail.expunge()
                            success = True
                            break
                        
                except Exception as e:
                    print(f"Error accessing trash folder {folder}: {e}")
                    continue
            
            if success:
                return f"Successfully emptied Gmail trash folder ({trash_folder_used}). Deleted {count} messages."
            else:
                return "Could not empty trash. No trash folder found or accessible."

        except Exception as e:
            return f"Error emptying trash: {str(e)}"
        finally:
            self._disconnect(mail)

================================================
FILE: src/gmail_crew_ai/tools/slack_tool.py
================================================
import os
import json
import requests
from typing import List, Dict, Optional, Type
from pydantic import BaseModel, Field
from crewai.tools import BaseTool

class SlackNotificationSchema(BaseModel):
    """Schema for SlackNotificationTool input."""
    subject: str = Field(..., description="Email subject")
    sender: str = Field(..., description="Email sender")
    category: str = Field(..., description="Email category")
    priority: str = Field(..., description="Email priority")
    summary: str = Field(..., description="Brief summary of the email content")
    action_needed: Optional[str] = Field(None, description="Action needed, if any")
    headline: Optional[str] = Field(None, description="Custom headline for the notification")
    intro: Optional[str] = Field(None, description="Custom intro phrase for the notification")
    action_header: Optional[str] = Field(None, description="Custom header for the action section")

class SlackNotificationTool(BaseTool):
    """Tool to send notifications to Slack."""
    name: str = "slack_notification"
    description: str = "Sends notifications about important emails to Slack"
    args_schema: Type[BaseModel] = SlackNotificationSchema
    
    class Config:
        arbitrary_types_allowed = True
    
    def __init__(self):
        super().__init__()
        self._webhook_url = os.environ.get("SLACK_WEBHOOK_URL")
        if not self._webhook_url:
            raise ValueError("SLACK_WEBHOOK_URL must be set in the environment.")

    def _run(self, subject: str, sender: str, category: str, 
             priority: str, summary: str, action_needed: Optional[str] = None,
             headline: Optional[str] = None, intro: Optional[str] = None,
             action_header: Optional[str] = None) -> str:
        """Send a notification to Slack."""
        
        # Format the message
        blocks = [
            {
                "type": "header",
                "text": {
                    "type": "plain_text",
                    "text": headline or f"Important Email: {subject}"
                }
            }
        ]
        
        # Add intro if provided
        if intro:
            blocks.append({
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f"*{intro}*"
                }
            })
        
        # Add email details
        blocks.append({
            "type": "section",
            "fields": [
                {"type": "mrkdwn", "text": f"*From:*\n{sender}"},
                {"type": "mrkdwn", "text": f"*Category:*\n{category}"},
                {"type": "mrkdwn", "text": f"*Priority:*\n{priority}"},
            ]
        })
        
        # Add summary
        blocks.append({
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": f"*Summary:*\n{summary}"
            }
        })
        
        # Add action needed if provided
        if action_needed:
            blocks.append({
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f"*{action_header or 'Action Needed:'}*\n{action_needed}"
                }
            })
        
        # Add divider
        blocks.append({"type": "divider"})
        
        # Prepare the payload
        payload = {
            "blocks": blocks
        }
        
        # Send the notification
        try:
            response = requests.post(
                self._webhook_url,
                data=json.dumps(payload),
                headers={"Content-Type": "application/json"}
            )
            response.raise_for_status()
            return f"Slack notification sent successfully for email: {subject}"
        except Exception as e:
            return f"Error sending Slack notification: {e}" 
Download .txt
gitextract_9jx_yv_y/

├── .env_example
├── .gitignore
├── LICENSE
├── README.md
├── knowledge/
│   └── user_preference.txt
├── pyproject.toml
└── src/
    └── gmail_crew_ai/
        ├── __init__.py
        ├── config/
        │   ├── agents.yaml
        │   └── tasks.yaml
        ├── crew.py
        ├── main.py
        ├── models.py
        └── tools/
            ├── __init__.py
            ├── date_tools.py
            ├── gmail_tools.py
            └── slack_tool.py
Download .txt
SYMBOL INDEX (64 symbols across 6 files)

FILE: src/gmail_crew_ai/crew.py
  class GmailCrewAi (line 16) | class GmailCrewAi():
    method fetch_emails (line 22) | def fetch_emails(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
    method categorizer (line 69) | def categorizer(self) -> Agent:
    method organizer (line 78) | def organizer(self) -> Agent:
    method response_generator (line 87) | def response_generator(self) -> Agent:
    method notifier (line 96) | def notifier(self) -> Agent:
    method cleaner (line 105) | def cleaner(self) -> Agent:
    method categorization_task (line 114) | def categorization_task(self) -> Task:
    method organization_task (line 122) | def organization_task(self) -> Task:
    method response_task (line 130) | def response_task(self) -> Task:
    method notification_task (line 138) | def notification_task(self) -> Task:
    method cleanup_task (line 146) | def cleanup_task(self) -> Task:
    method crew (line 154) | def crew(self) -> Crew:
    method _debug_callback (line 163) | def _debug_callback(self, event_type, payload):
    method _validate_categorization_output (line 193) | def _validate_categorization_output(self, output):

FILE: src/gmail_crew_ai/main.py
  function run (line 17) | def run():

FILE: src/gmail_crew_ai/models.py
  class EmailDetails (line 5) | class EmailDetails(BaseModel):
    method from_email_tuple (line 19) | def from_email_tuple(cls, email_tuple):
  class CategorizedEmail (line 49) | class CategorizedEmail(BaseModel):
  class OrganizedEmail (line 62) | class OrganizedEmail(BaseModel):
  class EmailResponse (line 70) | class EmailResponse(BaseModel):
  class SlackNotification (line 79) | class SlackNotification(BaseModel):
  class EmailCleanupInfo (line 93) | class EmailCleanupInfo(BaseModel):
  class SimpleCategorizedEmail (line 102) | class SimpleCategorizedEmail(BaseModel):
    method from_email_tuple (line 115) | def from_email_tuple(cls, email_tuple):

FILE: src/gmail_crew_ai/tools/date_tools.py
  class DateCalculationSchema (line 7) | class DateCalculationSchema(BaseModel):
  class DateCalculationTool (line 13) | class DateCalculationTool(BaseTool):
    method _run (line 19) | def _run(self, email_date: str, reference_date: Optional[str] = None) ...

FILE: src/gmail_crew_ai/tools/gmail_tools.py
  function decode_header_safe (line 16) | def decode_header_safe(header):
  function clean_email_body (line 38) | def clean_email_body(email_body: str) -> str:
  class GmailToolBase (line 53) | class GmailToolBase(BaseTool):
    class Config (line 56) | class Config:
    method __init__ (line 62) | def __init__(self, description: str = ""):
    method _connect (line 70) | def _connect(self):
    method _disconnect (line 82) | def _disconnect(self, mail):
    method _get_thread_messages (line 91) | def _get_thread_messages(self, mail: imaplib.IMAP4_SSL, msg) -> List[s...
    method _extract_body (line 117) | def _extract_body(self, msg) -> str:
  class GetUnreadEmailsSchema (line 141) | class GetUnreadEmailsSchema(BaseModel):
  class GetUnreadEmailsTool (line 149) | class GetUnreadEmailsTool(GmailToolBase):
    method _run (line 155) | def _run(self, limit: Optional[int] = 5) -> List[Tuple[str, str, str, ...
    method _parse_email_date (line 235) | def _parse_email_date(self, date_str: str) -> str:
  class SaveDraftSchema (line 257) | class SaveDraftSchema(BaseModel):
  class SaveDraftTool (line 264) | class SaveDraftTool(BaseTool):
    method _format_body (line 270) | def _format_body(self, body: str) -> str:
    method _connect (line 281) | def _connect(self):
    method _disconnect (line 296) | def _disconnect(self, mail):
    method _check_drafts_folder (line 303) | def _check_drafts_folder(self, mail):
    method _verify_draft_saved (line 316) | def _verify_draft_saved(self, mail, subject, recipient):
    method _run (line 354) | def _run(self, subject: str, body: str, recipient: str, thread_info: O...
  class GmailOrganizeSchema (line 449) | class GmailOrganizeSchema(BaseModel):
  class GmailOrganizeTool (line 457) | class GmailOrganizeTool(GmailToolBase):
    method _run (line 463) | def _run(self, email_id: str, category: str, priority: str, should_sta...
  class GmailDeleteSchema (line 507) | class GmailDeleteSchema(BaseModel):
  class GmailDeleteTool (line 512) | class GmailDeleteTool(BaseTool):
    method _run (line 517) | def _run(self, email_id: str, reason: str) -> str:
  class EmptyTrashTool (line 558) | class EmptyTrashTool(BaseTool):
    method _connect (line 563) | def _connect(self):
    method _disconnect (line 578) | def _disconnect(self, mail):
    method _run (line 585) | def _run(self) -> str:

FILE: src/gmail_crew_ai/tools/slack_tool.py
  class SlackNotificationSchema (line 8) | class SlackNotificationSchema(BaseModel):
  class SlackNotificationTool (line 20) | class SlackNotificationTool(BaseTool):
    class Config (line 26) | class Config:
    method __init__ (line 29) | def __init__(self):
    method _run (line 35) | def _run(self, subject: str, sender: str, category: str,
Condensed preview — 16 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (79K chars).
[
  {
    "path": ".env_example",
    "chars": 231,
    "preview": "# OpenAI Model\nMODEL=openai/gpt-4o-mini\nOPENAI_API_KEY=your_openai_api_key\n\n# Gmail credentials\nEMAIL_ADDRESS=your_email"
  },
  {
    "path": ".gitignore",
    "chars": 366,
    "preview": "# Environment variables\n.env\n.env.*\n\n# Python\n__pycache__/\n*.py[cod]\n*$py.class\n*.so\n.Python\nbuild/\ndevelop-eggs/\ndist/\n"
  },
  {
    "path": "LICENSE",
    "chars": 1071,
    "preview": "MIT License\n\nCopyright (c) 2025 Tony Kipkemboi\n\nPermission is hereby granted, free of charge, to any person obtaining a "
  },
  {
    "path": "README.md",
    "chars": 7739,
    "preview": "# Gmail Automation with CrewAI 📧✨\n\n[![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UC"
  },
  {
    "path": "knowledge/user_preference.txt",
    "chars": 124,
    "preview": "User name is John Doe.\nUser is an AI Engineer.\nUser is interested in AI Agents.\nUser is based in San Francisco, Californ"
  },
  {
    "path": "pyproject.toml",
    "chars": 568,
    "preview": "[project]\nname = \"gmail_crew_ai\"\nversion = \"0.1.0\"\ndescription = \"gmail-crew-ai using crewAI\"\nauthors = [{ name = \"Your "
  },
  {
    "path": "src/gmail_crew_ai/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/gmail_crew_ai/config/agents.yaml",
    "chars": 3433,
    "preview": "categorizer:\n  role: >\n    Email Categorizer and Prioritizer\n  goal: >\n    Analyze and categorize unread emails based on"
  },
  {
    "path": "src/gmail_crew_ai/config/tasks.yaml",
    "chars": 9670,
    "preview": "categorization_task:\n  description: >\n    First, read the fetched emails from the file at 'output/fetched_emails.json' u"
  },
  {
    "path": "src/gmail_crew_ai/crew.py",
    "chars": 8702,
    "preview": "from crewai import Agent, Crew, Process, Task, LLM\nfrom crewai.project import CrewBase, agent, crew, task, before_kickof"
  },
  {
    "path": "src/gmail_crew_ai/main.py",
    "chars": 2087,
    "preview": "#!/usr/bin/env python\nimport sys\nimport warnings\nfrom dotenv import load_dotenv\n\n# Remove or comment out these debug lin"
  },
  {
    "path": "src/gmail_crew_ai/models.py",
    "chars": 7228,
    "preview": "from pydantic import BaseModel, Field, SkipValidation\nfrom typing import List, Optional, Dict, Literal, Callable, Any\nfr"
  },
  {
    "path": "src/gmail_crew_ai/tools/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/gmail_crew_ai/tools/date_tools.py",
    "chars": 2244,
    "preview": "from datetime import datetime, date\nfrom typing import Optional\nfrom pydantic import BaseModel, Field\nfrom crewai.tools "
  },
  {
    "path": "src/gmail_crew_ai/tools/gmail_tools.py",
    "chars": 26701,
    "preview": "import imaplib\nimport email\nfrom email.header import decode_header\nfrom typing import List, Tuple, Literal, Optional, Ty"
  },
  {
    "path": "src/gmail_crew_ai/tools/slack_tool.py",
    "chars": 3875,
    "preview": "import os\nimport json\nimport requests\nfrom typing import List, Dict, Optional, Type\nfrom pydantic import BaseModel, Fiel"
  }
]

About this extraction

This page contains the full source code of the tonykipkemboi/crewai-gmail-automation GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 16 files (72.3 KB), approximately 16.6k tokens, and a symbol index with 64 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!