initiate
This commit is contained in:
104
CLAUDE.md
Normal file
104
CLAUDE.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# CLAUDE.md - Gmail Inbox Analyzer Project Guide
|
||||
|
||||
## Project Overview
|
||||
This is a **Gmail Inbox Analyzer** - a Go application that fetches email metadata (sender, subject, date, message ID) from a Gmail inbox using the Gmail API. The tool outputs data in CSV or JSON format for organization and analysis purposes.
|
||||
|
||||
## Architecture & Structure
|
||||
- **Single binary application** (`main.go`) - no complex module structure
|
||||
- **OAuth2 authentication** with Google's Gmail API
|
||||
- **Command-line interface** with three required arguments
|
||||
- **Two output formats**: CSV and JSON
|
||||
- **No persistent storage** - credentials are not stored locally
|
||||
|
||||
## Key Files
|
||||
- `/Users/orejav/repos/mail-automation/main.go` - Main application code (155 lines)
|
||||
- `/Users/orejav/repos/mail-automation/go.mod` - Go module definition with Gmail API dependencies
|
||||
- `/Users/orejav/repos/mail-automation/go.sum` - Dependency checksums
|
||||
- `/Users/orejav/repos/mail-automation/README.md` - User documentation and setup instructions
|
||||
|
||||
## Dependencies & Tech Stack
|
||||
- **Go 1.21** (minimum version)
|
||||
- **golang.org/x/oauth2** - OAuth2 authentication
|
||||
- **google.golang.org/api/gmail/v1** - Gmail API client
|
||||
- Standard library: `encoding/csv`, `encoding/json`, `net/http`, `context`
|
||||
|
||||
## Development Commands
|
||||
```bash
|
||||
# Install/update dependencies
|
||||
go mod tidy
|
||||
|
||||
# Run the application (requires Google OAuth credentials)
|
||||
go run main.go <CLIENT_ID> <CLIENT_SECRET> <csv|json>
|
||||
|
||||
# Example usage
|
||||
go run main.go "123456789-abc.apps.googleusercontent.com" "GOCSPX-your_secret" csv > emails.csv
|
||||
go run main.go "123456789-abc.apps.googleusercontent.com" "GOCSPX-your_secret" json > emails.json
|
||||
|
||||
# Build binary
|
||||
go build -o gmail-analyzer main.go
|
||||
```
|
||||
|
||||
## Authentication Flow
|
||||
1. Application generates OAuth2 URL for Gmail readonly access
|
||||
2. User visits URL in browser and authorizes access
|
||||
3. User copies authorization code from browser
|
||||
4. User pastes code into terminal
|
||||
5. Application exchanges code for access token
|
||||
6. Token is used for API calls (not persisted)
|
||||
|
||||
## Core Components
|
||||
1. **`main()`** - CLI argument parsing and orchestration
|
||||
2. **`getClient()`** - OAuth2 authentication flow
|
||||
3. **`fetchEmails()`** - Gmail API pagination and message retrieval
|
||||
4. **`parseMessage()`** - Extract metadata from Gmail message headers
|
||||
5. **`outputCSV()`** / **`outputJSON()`** - Data formatting and output
|
||||
|
||||
## Data Structure
|
||||
```go
|
||||
type EmailInfo struct {
|
||||
Sender string `json:"sender"`
|
||||
Subject string `json:"subject"`
|
||||
Date string `json:"date"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
```
|
||||
|
||||
## Google Cloud Setup Required
|
||||
- Google Cloud Console project
|
||||
- Gmail API enabled
|
||||
- OAuth 2.0 Client ID (Desktop application type)
|
||||
- Client ID and Client Secret credentials
|
||||
|
||||
## API Limitations & Behavior
|
||||
- Uses Gmail API readonly scope: `gmail.GmailReadonlyScope`
|
||||
- Fetches emails from inbox only (`in:inbox` query)
|
||||
- Processes all emails with pagination (no date/count limits)
|
||||
- Rate limiting handled by Google's client library
|
||||
- No email content/body is fetched - headers only
|
||||
|
||||
## Common Tasks for Claude
|
||||
1. **Modify output format** - Edit `outputCSV()` or `outputJSON()` functions
|
||||
2. **Add filtering** - Modify Gmail query in `fetchEmails()` function
|
||||
3. **Add new metadata fields** - Update `EmailInfo` struct and `parseMessage()`
|
||||
4. **Error handling improvements** - Add retry logic or better error messages
|
||||
5. **Configuration** - Add config file support or environment variables
|
||||
6. **Batch processing** - Add date range or count limits
|
||||
|
||||
## Testing & Debugging
|
||||
- No unit tests currently exist
|
||||
- Manual testing requires valid Google OAuth credentials
|
||||
- Debug by checking Gmail API quotas in Google Cloud Console
|
||||
- Common issues: invalid credentials, API not enabled, rate limiting
|
||||
|
||||
## Security Considerations
|
||||
- OAuth credentials passed as command-line arguments (visible in process list)
|
||||
- No credential persistence (security by design)
|
||||
- Readonly Gmail access only
|
||||
- Consider environment variables for credentials in production use
|
||||
|
||||
## Extension Points
|
||||
- Add support for other Gmail folders/labels
|
||||
- Implement credential caching (with proper security)
|
||||
- Add filtering by date range, sender, or subject patterns
|
||||
- Export to other formats (Excel, database)
|
||||
- Add email content analysis capabilities
|
||||
102
README.md
Normal file
102
README.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Gmail Inbox Analyzer
|
||||
|
||||
A Go script to fetch sender and subject information from your Gmail inbox for organization purposes.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||
2. Create a new project or select an existing one
|
||||
3. Enable the Gmail API
|
||||
4. Go to "Credentials" → "Create Credentials" → "OAuth 2.0 Client IDs"
|
||||
5. Choose "Desktop application"
|
||||
6. Note down your Client ID and Client Secret
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
go mod tidy
|
||||
go run main.go <CLIENT_ID> <CLIENT_SECRET> <csv|json>
|
||||
```
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
1. The script will display a URL
|
||||
2. Visit the URL in your browser and log into your Google account
|
||||
3. Copy the authorization code from the browser
|
||||
4. Paste it back into the terminal
|
||||
|
||||
### Output Formats
|
||||
|
||||
- **CSV**: `go run main.go CLIENT_ID CLIENT_SECRET csv > emails.csv`
|
||||
- **JSON**: `go run main.go CLIENT_ID CLIENT_SECRET json > emails.json`
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
go run main.go "123456789-abc.apps.googleusercontent.com" "GOCSPX-your_secret" csv > my_emails.csv
|
||||
```
|
||||
|
||||
The script fetches all emails from your inbox with:
|
||||
- Sender email address
|
||||
- Subject line
|
||||
- Date
|
||||
- Message ID
|
||||
|
||||
No credentials are stored locally - authentication is required each time you run the script.
|
||||
|
||||
## Organization Tools
|
||||
|
||||
After exporting your email data, use these additional tools to organize your inbox:
|
||||
|
||||
### 1. Email Analysis
|
||||
Analyze patterns in your exported data:
|
||||
```bash
|
||||
go run analyze.go emails.csv summary
|
||||
go run analyze.go emails.csv json > analysis.json
|
||||
```
|
||||
|
||||
### 2. Gmail Filter Generator
|
||||
Generate Gmail filters based on your email patterns:
|
||||
```bash
|
||||
go run filters.go analysis.json filters # Show filter suggestions
|
||||
go run filters.go analysis.json queries # Show search queries
|
||||
go run filters.go analysis.json all # Show everything
|
||||
```
|
||||
|
||||
### 3. Cleanup Recommendations
|
||||
Get specific cleanup recommendations:
|
||||
```bash
|
||||
go run cleanup.go analysis.json summary # Quick overview
|
||||
go run cleanup.go analysis.json detailed # Full instructions
|
||||
```
|
||||
|
||||
### 4. Interactive Organization Wizard
|
||||
Run the complete organization workflow:
|
||||
```bash
|
||||
go run organize.go emails.csv
|
||||
```
|
||||
|
||||
This will guide you through:
|
||||
- Email analysis
|
||||
- Organization approach selection (aggressive cleanup, filters-focused, balanced, or analysis-only)
|
||||
- Step-by-step implementation
|
||||
|
||||
## Workflow Example
|
||||
|
||||
```bash
|
||||
# 1. Export your Gmail data
|
||||
go run main.go "your-client-id" "your-secret" csv > emails.csv
|
||||
|
||||
# 2. Run the organization wizard
|
||||
go run organize.go emails.csv
|
||||
|
||||
# 3. Follow the interactive prompts to organize your inbox
|
||||
```
|
||||
|
||||
The tools will help you:
|
||||
- Identify top senders and domains
|
||||
- Detect email patterns (newsletters, promotions, etc.)
|
||||
- Generate Gmail filters for automatic organization
|
||||
- Find emails safe to delete or archive
|
||||
- Create unsubscribe recommendations
|
||||
- Provide specific Gmail search queries for cleanup
|
||||
443
analyze.go
Normal file
443
analyze.go
Normal file
@@ -0,0 +1,443 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type EmailStats struct {
|
||||
TotalEmails int `json:"total_emails"`
|
||||
TopSenders []SenderInfo `json:"top_senders"`
|
||||
TopDomains []DomainInfo `json:"top_domains"`
|
||||
Categories map[string]int `json:"categories"`
|
||||
SubjectPatterns []PatternInfo `json:"subject_patterns"`
|
||||
TimeAnalysis TimeStats `json:"time_analysis"`
|
||||
}
|
||||
|
||||
type SenderInfo struct {
|
||||
Email string `json:"email"`
|
||||
Count int `json:"count"`
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type DomainInfo struct {
|
||||
Domain string `json:"domain"`
|
||||
Count int `json:"count"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type PatternInfo struct {
|
||||
Pattern string `json:"pattern"`
|
||||
Count int `json:"count"`
|
||||
Examples []string `json:"examples"`
|
||||
}
|
||||
|
||||
type TimeStats struct {
|
||||
EmailsByYear map[string]int `json:"emails_by_year"`
|
||||
EmailsByMonth map[string]int `json:"emails_by_month"`
|
||||
OldestEmail string `json:"oldest_email"`
|
||||
NewestEmail string `json:"newest_email"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("Usage: go run analyze.go <csv_file> [output_format]")
|
||||
fmt.Println(" csv_file: path to CSV file from main.go")
|
||||
fmt.Println(" output_format: json (default) or summary")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
csvFile := os.Args[1]
|
||||
outputFormat := "json"
|
||||
if len(os.Args) > 2 {
|
||||
outputFormat = strings.ToLower(os.Args[2])
|
||||
}
|
||||
|
||||
emails, err := loadEmailsFromCSV(csvFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading CSV: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Analyzing %d emails...\n", len(emails))
|
||||
|
||||
stats := analyzeEmails(emails)
|
||||
|
||||
switch outputFormat {
|
||||
case "summary":
|
||||
printSummary(stats)
|
||||
case "json":
|
||||
outputJSON(stats)
|
||||
default:
|
||||
fmt.Printf("Unknown output format: %s\n", outputFormat)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func loadEmailsFromCSV(filename string) ([]EmailInfo, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
reader := csv.NewReader(file)
|
||||
records, err := reader.ReadAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var emails []EmailInfo
|
||||
for i, record := range records {
|
||||
if i == 0 && record[0] == "Sender" {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(record) >= 4 {
|
||||
emails = append(emails, EmailInfo{
|
||||
Sender: record[0],
|
||||
Subject: record[1],
|
||||
Date: record[2],
|
||||
ID: record[3],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return emails, nil
|
||||
}
|
||||
|
||||
func analyzeEmails(emails []EmailInfo) EmailStats {
|
||||
stats := EmailStats{
|
||||
TotalEmails: len(emails),
|
||||
Categories: make(map[string]int),
|
||||
TimeAnalysis: TimeStats{
|
||||
EmailsByYear: make(map[string]int),
|
||||
EmailsByMonth: make(map[string]int),
|
||||
},
|
||||
}
|
||||
|
||||
senderCounts := make(map[string]int)
|
||||
domainCounts := make(map[string]int)
|
||||
patternCounts := make(map[string][]string)
|
||||
|
||||
var oldestTime, newestTime time.Time
|
||||
|
||||
for _, email := range emails {
|
||||
// Sender analysis
|
||||
senderCounts[email.Sender]++
|
||||
|
||||
// Domain analysis
|
||||
domain := extractDomain(email.Sender)
|
||||
if domain != "" {
|
||||
domainCounts[domain]++
|
||||
}
|
||||
|
||||
// Subject pattern analysis
|
||||
patterns := detectSubjectPatterns(email.Subject)
|
||||
for _, pattern := range patterns {
|
||||
patternCounts[pattern] = append(patternCounts[pattern], email.Subject)
|
||||
}
|
||||
|
||||
// Category analysis
|
||||
category := categorizeEmail(email.Sender, email.Subject)
|
||||
stats.Categories[category]++
|
||||
|
||||
// Time analysis
|
||||
if emailTime, err := parseEmailDate(email.Date); err == nil {
|
||||
year := emailTime.Format("2006")
|
||||
month := emailTime.Format("2006-01")
|
||||
|
||||
stats.TimeAnalysis.EmailsByYear[year]++
|
||||
stats.TimeAnalysis.EmailsByMonth[month]++
|
||||
|
||||
if oldestTime.IsZero() || emailTime.Before(oldestTime) {
|
||||
oldestTime = emailTime
|
||||
stats.TimeAnalysis.OldestEmail = email.Date
|
||||
}
|
||||
if newestTime.IsZero() || emailTime.After(newestTime) {
|
||||
newestTime = emailTime
|
||||
stats.TimeAnalysis.NewestEmail = email.Date
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert maps to sorted slices
|
||||
stats.TopSenders = sortSenders(senderCounts)
|
||||
stats.TopDomains = sortDomains(domainCounts)
|
||||
stats.SubjectPatterns = sortPatterns(patternCounts)
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
func extractDomain(email string) string {
|
||||
parts := strings.Split(email, "@")
|
||||
if len(parts) != 2 {
|
||||
// Handle cases like "Name <email@domain.com>"
|
||||
re := regexp.MustCompile(`<([^@]+@[^>]+)>`)
|
||||
matches := re.FindStringSubmatch(email)
|
||||
if len(matches) > 1 {
|
||||
parts = strings.Split(matches[1], "@")
|
||||
if len(parts) == 2 {
|
||||
return strings.ToLower(strings.TrimSpace(parts[1]))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return strings.ToLower(strings.TrimSpace(parts[1]))
|
||||
}
|
||||
|
||||
func detectSubjectPatterns(subject string) []string {
|
||||
var patterns []string
|
||||
|
||||
subject = strings.ToLower(subject)
|
||||
|
||||
// Newsletter patterns
|
||||
if strings.Contains(subject, "newsletter") || strings.Contains(subject, "weekly") ||
|
||||
strings.Contains(subject, "monthly") || strings.Contains(subject, "digest") {
|
||||
patterns = append(patterns, "newsletter")
|
||||
}
|
||||
|
||||
// Automated patterns
|
||||
if strings.HasPrefix(subject, "re:") {
|
||||
patterns = append(patterns, "reply")
|
||||
}
|
||||
if strings.HasPrefix(subject, "fwd:") || strings.HasPrefix(subject, "fw:") {
|
||||
patterns = append(patterns, "forward")
|
||||
}
|
||||
|
||||
// Notification patterns
|
||||
if strings.Contains(subject, "notification") || strings.Contains(subject, "alert") ||
|
||||
strings.Contains(subject, "reminder") {
|
||||
patterns = append(patterns, "notification")
|
||||
}
|
||||
|
||||
// Commercial patterns
|
||||
if strings.Contains(subject, "sale") || strings.Contains(subject, "deal") ||
|
||||
strings.Contains(subject, "offer") || strings.Contains(subject, "discount") ||
|
||||
strings.Contains(subject, "%") || strings.Contains(subject, "free") {
|
||||
patterns = append(patterns, "promotional")
|
||||
}
|
||||
|
||||
// Update patterns
|
||||
if strings.Contains(subject, "update") || strings.Contains(subject, "new version") ||
|
||||
strings.Contains(subject, "release") {
|
||||
patterns = append(patterns, "update")
|
||||
}
|
||||
|
||||
// Receipt/confirmation patterns
|
||||
if strings.Contains(subject, "receipt") || strings.Contains(subject, "confirmation") ||
|
||||
strings.Contains(subject, "invoice") || strings.Contains(subject, "payment") {
|
||||
patterns = append(patterns, "transactional")
|
||||
}
|
||||
|
||||
return patterns
|
||||
}
|
||||
|
||||
func categorizeEmail(sender, subject string) string {
|
||||
domain := extractDomain(sender)
|
||||
senderLower := strings.ToLower(sender)
|
||||
subjectLower := strings.ToLower(subject)
|
||||
|
||||
// Social networks
|
||||
socialDomains := []string{"facebook.com", "twitter.com", "linkedin.com", "instagram.com",
|
||||
"tiktok.com", "youtube.com", "reddit.com"}
|
||||
for _, social := range socialDomains {
|
||||
if strings.Contains(domain, social) {
|
||||
return "social"
|
||||
}
|
||||
}
|
||||
|
||||
// Financial
|
||||
if strings.Contains(subjectLower, "payment") || strings.Contains(subjectLower, "invoice") ||
|
||||
strings.Contains(subjectLower, "receipt") || strings.Contains(domain, "bank") ||
|
||||
strings.Contains(domain, "paypal") || strings.Contains(domain, "stripe") {
|
||||
return "finance"
|
||||
}
|
||||
|
||||
// Travel
|
||||
if strings.Contains(domain, "booking") || strings.Contains(domain, "airbnb") ||
|
||||
strings.Contains(domain, "hotel") || strings.Contains(domain, "airline") ||
|
||||
strings.Contains(subjectLower, "flight") || strings.Contains(subjectLower, "reservation") {
|
||||
return "travel"
|
||||
}
|
||||
|
||||
// Shopping
|
||||
if strings.Contains(domain, "amazon") || strings.Contains(domain, "ebay") ||
|
||||
strings.Contains(subjectLower, "order") || strings.Contains(subjectLower, "shipping") {
|
||||
return "shopping"
|
||||
}
|
||||
|
||||
// Newsletters/Marketing
|
||||
if strings.Contains(senderLower, "noreply") || strings.Contains(senderLower, "no-reply") ||
|
||||
strings.Contains(subjectLower, "newsletter") || strings.Contains(subjectLower, "unsubscribe") {
|
||||
return "newsletters"
|
||||
}
|
||||
|
||||
// Work-related
|
||||
if strings.Contains(domain, "slack") || strings.Contains(domain, "github") ||
|
||||
strings.Contains(domain, "jira") || strings.Contains(domain, "atlassian") {
|
||||
return "work"
|
||||
}
|
||||
|
||||
return "personal"
|
||||
}
|
||||
|
||||
func parseEmailDate(dateStr string) (time.Time, error) {
|
||||
formats := []string{
|
||||
time.RFC1123Z,
|
||||
time.RFC1123,
|
||||
"Mon, 2 Jan 2006 15:04:05 -0700",
|
||||
"2 Jan 2006 15:04:05 -0700",
|
||||
"2006-01-02T15:04:05Z07:00",
|
||||
"2006-01-02 15:04:05",
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
if t, err := time.Parse(format, dateStr); err == nil {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
|
||||
return time.Time{}, fmt.Errorf("unable to parse date: %s", dateStr)
|
||||
}
|
||||
|
||||
func sortSenders(senderCounts map[string]int) []SenderInfo {
|
||||
var senders []SenderInfo
|
||||
for email, count := range senderCounts {
|
||||
senders = append(senders, SenderInfo{
|
||||
Email: email,
|
||||
Count: count,
|
||||
Domain: extractDomain(email),
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(senders, func(i, j int) bool {
|
||||
return senders[i].Count > senders[j].Count
|
||||
})
|
||||
|
||||
if len(senders) > 20 {
|
||||
senders = senders[:20]
|
||||
}
|
||||
|
||||
return senders
|
||||
}
|
||||
|
||||
func sortDomains(domainCounts map[string]int) []DomainInfo {
|
||||
var domains []DomainInfo
|
||||
for domain, count := range domainCounts {
|
||||
domainType := categorizeDomain(domain)
|
||||
domains = append(domains, DomainInfo{
|
||||
Domain: domain,
|
||||
Count: count,
|
||||
Type: domainType,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(domains, func(i, j int) bool {
|
||||
return domains[i].Count > domains[j].Count
|
||||
})
|
||||
|
||||
if len(domains) > 15 {
|
||||
domains = domains[:15]
|
||||
}
|
||||
|
||||
return domains
|
||||
}
|
||||
|
||||
func categorizeDomain(domain string) string {
|
||||
domain = strings.ToLower(domain)
|
||||
|
||||
if strings.Contains(domain, "gmail") || strings.Contains(domain, "yahoo") ||
|
||||
strings.Contains(domain, "hotmail") || strings.Contains(domain, "outlook") {
|
||||
return "personal"
|
||||
}
|
||||
|
||||
if strings.Contains(domain, "facebook") || strings.Contains(domain, "twitter") ||
|
||||
strings.Contains(domain, "linkedin") || strings.Contains(domain, "instagram") {
|
||||
return "social"
|
||||
}
|
||||
|
||||
if strings.Contains(domain, "amazon") || strings.Contains(domain, "ebay") ||
|
||||
strings.Contains(domain, "shop") || strings.Contains(domain, "store") {
|
||||
return "commerce"
|
||||
}
|
||||
|
||||
if strings.Contains(domain, "noreply") || strings.Contains(domain, "no-reply") ||
|
||||
strings.Contains(domain, "mail") {
|
||||
return "automated"
|
||||
}
|
||||
|
||||
return "business"
|
||||
}
|
||||
|
||||
func sortPatterns(patternCounts map[string][]string) []PatternInfo {
|
||||
var patterns []PatternInfo
|
||||
for pattern, examples := range patternCounts {
|
||||
// Limit examples to 3
|
||||
limitedExamples := examples
|
||||
if len(limitedExamples) > 3 {
|
||||
limitedExamples = limitedExamples[:3]
|
||||
}
|
||||
|
||||
patterns = append(patterns, PatternInfo{
|
||||
Pattern: pattern,
|
||||
Count: len(examples),
|
||||
Examples: limitedExamples,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(patterns, func(i, j int) bool {
|
||||
return patterns[i].Count > patterns[j].Count
|
||||
})
|
||||
|
||||
return patterns
|
||||
}
|
||||
|
||||
func printSummary(stats EmailStats) {
|
||||
fmt.Printf("\n=== EMAIL ANALYSIS SUMMARY ===\n")
|
||||
fmt.Printf("Total emails analyzed: %d\n\n", stats.TotalEmails)
|
||||
|
||||
fmt.Printf("TOP SENDERS:\n")
|
||||
for i, sender := range stats.TopSenders {
|
||||
if i >= 10 {
|
||||
break
|
||||
}
|
||||
fmt.Printf(" %d. %s (%d emails)\n", i+1, sender.Email, sender.Count)
|
||||
}
|
||||
|
||||
fmt.Printf("\nTOP DOMAINS:\n")
|
||||
for i, domain := range stats.TopDomains {
|
||||
if i >= 10 {
|
||||
break
|
||||
}
|
||||
fmt.Printf(" %d. %s (%d emails, %s)\n", i+1, domain.Domain, domain.Count, domain.Type)
|
||||
}
|
||||
|
||||
fmt.Printf("\nEMAIL CATEGORIES:\n")
|
||||
for category, count := range stats.Categories {
|
||||
percentage := float64(count) / float64(stats.TotalEmails) * 100
|
||||
fmt.Printf(" %s: %d emails (%.1f%%)\n", category, count, percentage)
|
||||
}
|
||||
|
||||
fmt.Printf("\nSUBJECT PATTERNS:\n")
|
||||
for _, pattern := range stats.SubjectPatterns {
|
||||
fmt.Printf(" %s: %d emails\n", pattern.Pattern, pattern.Count)
|
||||
}
|
||||
|
||||
fmt.Printf("\nTIME ANALYSIS:\n")
|
||||
fmt.Printf(" Date range: %s to %s\n", stats.TimeAnalysis.OldestEmail, stats.TimeAnalysis.NewestEmail)
|
||||
fmt.Printf(" Years with emails: %d\n", len(stats.TimeAnalysis.EmailsByYear))
|
||||
}
|
||||
|
||||
func outputJSON(stats EmailStats) {
|
||||
encoder := json.NewEncoder(os.Stdout)
|
||||
encoder.SetIndent("", " ")
|
||||
encoder.Encode(stats)
|
||||
}
|
||||
500
cleanup.go
Normal file
500
cleanup.go
Normal file
@@ -0,0 +1,500 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type CleanupRecommendations struct {
|
||||
SafeToDelete []DeleteRecommendation `json:"safe_to_delete"`
|
||||
BulkArchive []ArchiveRecommendation `json:"bulk_archive"`
|
||||
UnsubscribeTargets []UnsubscribeTarget `json:"unsubscribe_targets"`
|
||||
AttentionNeeded []AttentionItem `json:"attention_needed"`
|
||||
StorageStats StorageStats `json:"storage_stats"`
|
||||
}
|
||||
|
||||
type DeleteRecommendation struct {
|
||||
Category string `json:"category"`
|
||||
Query string `json:"query"`
|
||||
Count int `json:"estimated_count"`
|
||||
Description string `json:"description"`
|
||||
Risk string `json:"risk"`
|
||||
Examples []string `json:"examples"`
|
||||
}
|
||||
|
||||
type ArchiveRecommendation struct {
|
||||
Category string `json:"category"`
|
||||
Query string `json:"query"`
|
||||
Count int `json:"estimated_count"`
|
||||
Description string `json:"description"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type UnsubscribeTarget struct {
|
||||
Sender string `json:"sender"`
|
||||
Count int `json:"count"`
|
||||
LastEmail string `json:"last_email"`
|
||||
Category string `json:"category"`
|
||||
Priority string `json:"priority"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type AttentionItem struct {
|
||||
Category string `json:"category"`
|
||||
Query string `json:"query"`
|
||||
Count int `json:"count"`
|
||||
Description string `json:"description"`
|
||||
Action string `json:"recommended_action"`
|
||||
}
|
||||
|
||||
type StorageStats struct {
|
||||
TotalEmails int `json:"total_emails"`
|
||||
DeletionPotential int `json:"deletion_potential"`
|
||||
ArchivePotential int `json:"archive_potential"`
|
||||
PercentageReduction float64 `json:"percentage_reduction"`
|
||||
CategoryBreakdown map[string]int `json:"category_breakdown"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("Usage: go run cleanup.go <analysis_json_file> [output_format]")
|
||||
fmt.Println(" analysis_json_file: JSON output from analyze.go")
|
||||
fmt.Println(" output_format: summary (default), detailed, or json")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
jsonFile := os.Args[1]
|
||||
outputFormat := "summary"
|
||||
if len(os.Args) > 2 {
|
||||
outputFormat = strings.ToLower(os.Args[2])
|
||||
}
|
||||
|
||||
stats, err := loadAnalysisFromJSON(jsonFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading analysis JSON: %v", err)
|
||||
}
|
||||
|
||||
recommendations := generateCleanupRecommendations(stats)
|
||||
|
||||
switch outputFormat {
|
||||
case "summary":
|
||||
printSummary(recommendations)
|
||||
case "detailed":
|
||||
printDetailed(recommendations)
|
||||
case "json":
|
||||
outputJSON(recommendations)
|
||||
default:
|
||||
fmt.Printf("Unknown output format: %s\n", outputFormat)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func loadAnalysisFromJSON(filename string) (EmailStats, error) {
|
||||
var stats EmailStats
|
||||
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return stats, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
decoder := json.NewDecoder(file)
|
||||
err = decoder.Decode(&stats)
|
||||
return stats, err
|
||||
}
|
||||
|
||||
func generateCleanupRecommendations(stats EmailStats) CleanupRecommendations {
|
||||
recommendations := CleanupRecommendations{
|
||||
SafeToDelete: []DeleteRecommendation{},
|
||||
BulkArchive: []ArchiveRecommendation{},
|
||||
UnsubscribeTargets: []UnsubscribeTarget{},
|
||||
AttentionNeeded: []AttentionItem{},
|
||||
StorageStats: StorageStats{
|
||||
TotalEmails: stats.TotalEmails,
|
||||
CategoryBreakdown: stats.Categories,
|
||||
},
|
||||
}
|
||||
|
||||
// Generate safe delete recommendations
|
||||
recommendations.SafeToDelete = generateDeleteRecommendations(stats)
|
||||
|
||||
// Generate bulk archive recommendations
|
||||
recommendations.BulkArchive = generateArchiveRecommendations(stats)
|
||||
|
||||
// Generate unsubscribe targets
|
||||
recommendations.UnsubscribeTargets = generateUnsubscribeTargets(stats)
|
||||
|
||||
// Generate attention needed items
|
||||
recommendations.AttentionNeeded = generateAttentionItems(stats)
|
||||
|
||||
// Calculate storage impact
|
||||
recommendations.StorageStats = calculateStorageImpact(stats, recommendations)
|
||||
|
||||
return recommendations
|
||||
}
|
||||
|
||||
func generateDeleteRecommendations(stats EmailStats) []DeleteRecommendation {
|
||||
var recommendations []DeleteRecommendation
|
||||
|
||||
// Old promotional emails
|
||||
if count, ok := stats.Categories["newsletters"]; ok && count > 50 {
|
||||
recommendations = append(recommendations, DeleteRecommendation{
|
||||
Category: "Old Newsletters",
|
||||
Query: `subject:(newsletter OR weekly OR monthly) older_than:6m`,
|
||||
Count: count / 2, // Estimate half are old
|
||||
Description: "Newsletter emails older than 6 months",
|
||||
Risk: "Low",
|
||||
Examples: []string{"Weekly digest emails", "Monthly newsletters", "Company updates"},
|
||||
})
|
||||
}
|
||||
|
||||
// Old social media notifications
|
||||
if socialCount := countSocialEmails(stats.TopDomains); socialCount > 30 {
|
||||
recommendations = append(recommendations, DeleteRecommendation{
|
||||
Category: "Social Media Notifications",
|
||||
Query: `from:(facebook OR twitter OR linkedin OR instagram) older_than:2m`,
|
||||
Count: socialCount * 2 / 3, // Estimate 2/3 are old
|
||||
Description: "Social media notifications older than 2 months",
|
||||
Risk: "Low",
|
||||
Examples: []string{"Facebook notifications", "Twitter alerts", "LinkedIn updates"},
|
||||
})
|
||||
}
|
||||
|
||||
// Old promotional emails
|
||||
promotionalCount := 0
|
||||
for _, pattern := range stats.SubjectPatterns {
|
||||
if pattern.Pattern == "promotional" {
|
||||
promotionalCount = pattern.Count
|
||||
break
|
||||
}
|
||||
}
|
||||
if promotionalCount > 20 {
|
||||
recommendations = append(recommendations, DeleteRecommendation{
|
||||
Category: "Old Promotions",
|
||||
Query: `subject:(sale OR deal OR offer OR discount) older_than:3m`,
|
||||
Count: promotionalCount * 3 / 4, // Estimate 3/4 are old
|
||||
Description: "Promotional/sales emails older than 3 months",
|
||||
Risk: "Low",
|
||||
Examples: []string{"Sales announcements", "Discount offers", "Flash sales"},
|
||||
})
|
||||
}
|
||||
|
||||
// Automated system emails
|
||||
automatedCount := countAutomatedEmails(stats.TopDomains)
|
||||
if automatedCount > 40 {
|
||||
recommendations = append(recommendations, DeleteRecommendation{
|
||||
Category: "Old System Notifications",
|
||||
Query: `from:(noreply OR no-reply) subject:(notification OR alert) older_than:1y`,
|
||||
Count: automatedCount / 2,
|
||||
Description: "System notifications and alerts older than 1 year",
|
||||
Risk: "Medium",
|
||||
Examples: []string{"System alerts", "Automated notifications", "Service updates"},
|
||||
})
|
||||
}
|
||||
|
||||
return recommendations
|
||||
}
|
||||
|
||||
func generateArchiveRecommendations(stats EmailStats) []ArchiveRecommendation {
|
||||
var recommendations []ArchiveRecommendation
|
||||
|
||||
// Archive newsletters
|
||||
if count, ok := stats.Categories["newsletters"]; ok && count > 30 {
|
||||
recommendations = append(recommendations, ArchiveRecommendation{
|
||||
Category: "All Newsletters",
|
||||
Query: `from:(noreply OR no-reply) OR subject:(newsletter OR unsubscribe)`,
|
||||
Count: count,
|
||||
Description: "Move all newsletter-type emails out of inbox",
|
||||
Reason: "Newsletters rarely require immediate action",
|
||||
})
|
||||
}
|
||||
|
||||
// Archive social media
|
||||
if count, ok := stats.Categories["social"]; ok && count > 20 {
|
||||
recommendations = append(recommendations, ArchiveRecommendation{
|
||||
Category: "Social Media",
|
||||
Query: `from:(facebook OR twitter OR linkedin OR instagram OR youtube)`,
|
||||
Count: count,
|
||||
Description: "Move social media notifications out of inbox",
|
||||
Reason: "Social notifications can be checked on the platforms directly",
|
||||
})
|
||||
}
|
||||
|
||||
// Archive automated emails
|
||||
automatedCount := countAutomatedEmails(stats.TopDomains)
|
||||
if automatedCount > 25 {
|
||||
recommendations = append(recommendations, ArchiveRecommendation{
|
||||
Category: "Automated Emails",
|
||||
Query: `from:(noreply OR no-reply OR automated)`,
|
||||
Count: automatedCount,
|
||||
Description: "Move automated system emails out of inbox",
|
||||
Reason: "Automated emails are usually informational only",
|
||||
})
|
||||
}
|
||||
|
||||
return recommendations
|
||||
}
|
||||
|
||||
func generateUnsubscribeTargets(stats EmailStats) []UnsubscribeTarget {
|
||||
var targets []UnsubscribeTarget
|
||||
|
||||
// High-volume newsletter senders
|
||||
for _, sender := range stats.TopSenders {
|
||||
if sender.Count <= 10 {
|
||||
break
|
||||
}
|
||||
|
||||
priority := "medium"
|
||||
reason := ""
|
||||
category := ""
|
||||
|
||||
email := strings.ToLower(sender.Email)
|
||||
|
||||
if strings.Contains(email, "noreply") || strings.Contains(email, "no-reply") {
|
||||
category = "automated"
|
||||
if sender.Count > 50 {
|
||||
priority = "high"
|
||||
reason = "Very high volume automated sender"
|
||||
} else if sender.Count > 25 {
|
||||
priority = "medium"
|
||||
reason = "High volume automated sender"
|
||||
} else {
|
||||
priority = "low"
|
||||
reason = "Moderate volume automated sender"
|
||||
}
|
||||
} else if strings.Contains(email, "newsletter") || strings.Contains(email, "marketing") {
|
||||
category = "newsletter"
|
||||
priority = "medium"
|
||||
reason = "Newsletter or marketing emails"
|
||||
} else if containsSocialDomain(sender.Domain) {
|
||||
category = "social"
|
||||
priority = "low"
|
||||
reason = "Social media notifications"
|
||||
} else {
|
||||
category = "other"
|
||||
priority = "low"
|
||||
reason = "High volume sender - review manually"
|
||||
}
|
||||
|
||||
if sender.Count > 15 {
|
||||
targets = append(targets, UnsubscribeTarget{
|
||||
Sender: sender.Email,
|
||||
Count: sender.Count,
|
||||
Category: category,
|
||||
Priority: priority,
|
||||
Reason: reason,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by priority and count
|
||||
sort.Slice(targets, func(i, j int) bool {
|
||||
if targets[i].Priority != targets[j].Priority {
|
||||
priorityOrder := map[string]int{"high": 3, "medium": 2, "low": 1}
|
||||
return priorityOrder[targets[i].Priority] > priorityOrder[targets[j].Priority]
|
||||
}
|
||||
return targets[i].Count > targets[j].Count
|
||||
})
|
||||
|
||||
return targets
|
||||
}
|
||||
|
||||
func generateAttentionItems(stats EmailStats) []AttentionItem {
|
||||
var items []AttentionItem
|
||||
|
||||
// High volume personal senders
|
||||
for _, sender := range stats.TopSenders {
|
||||
if sender.Count > 50 && !strings.Contains(strings.ToLower(sender.Email), "noreply") &&
|
||||
!containsAutomatedKeywords(sender.Email) {
|
||||
items = append(items, AttentionItem{
|
||||
Category: "High Volume Personal",
|
||||
Query: fmt.Sprintf(`from:%s`, sender.Email),
|
||||
Count: sender.Count,
|
||||
Description: fmt.Sprintf("Very high email volume from %s", sender.Email),
|
||||
Action: "Review relationship or create filters",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Old unread emails
|
||||
items = append(items, AttentionItem{
|
||||
Category: "Old Unread",
|
||||
Query: `is:unread older_than:1m`,
|
||||
Count: 0, // Would need additional analysis
|
||||
Description: "Unread emails older than 1 month",
|
||||
Action: "Review and either read, archive, or delete",
|
||||
})
|
||||
|
||||
// Large attachments
|
||||
items = append(items, AttentionItem{
|
||||
Category: "Large Attachments",
|
||||
Query: `has:attachment larger:10M`,
|
||||
Count: 0, // Would need additional analysis
|
||||
Description: "Emails with attachments larger than 10MB",
|
||||
Action: "Review and download important files, then delete emails",
|
||||
})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func calculateStorageImpact(stats EmailStats, recommendations CleanupRecommendations) StorageStats {
|
||||
deletionPotential := 0
|
||||
archivePotential := 0
|
||||
|
||||
for _, rec := range recommendations.SafeToDelete {
|
||||
deletionPotential += rec.Count
|
||||
}
|
||||
|
||||
for _, rec := range recommendations.BulkArchive {
|
||||
archivePotential += rec.Count
|
||||
}
|
||||
|
||||
totalReduction := deletionPotential + archivePotential
|
||||
percentageReduction := float64(totalReduction) / float64(stats.TotalEmails) * 100
|
||||
|
||||
return StorageStats{
|
||||
TotalEmails: stats.TotalEmails,
|
||||
DeletionPotential: deletionPotential,
|
||||
ArchivePotential: archivePotential,
|
||||
PercentageReduction: percentageReduction,
|
||||
CategoryBreakdown: stats.Categories,
|
||||
}
|
||||
}
|
||||
|
||||
func countSocialEmails(domains []DomainInfo) int {
|
||||
count := 0
|
||||
for _, domain := range domains {
|
||||
if domain.Type == "social" {
|
||||
count += domain.Count
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func countAutomatedEmails(domains []DomainInfo) int {
|
||||
count := 0
|
||||
for _, domain := range domains {
|
||||
if domain.Type == "automated" {
|
||||
count += domain.Count
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func containsSocialDomain(domain string) bool {
|
||||
socialDomains := []string{"facebook", "twitter", "linkedin", "instagram", "youtube", "tiktok"}
|
||||
domain = strings.ToLower(domain)
|
||||
for _, social := range socialDomains {
|
||||
if strings.Contains(domain, social) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsAutomatedKeywords(email string) bool {
|
||||
keywords := []string{"noreply", "no-reply", "automated", "system", "admin"}
|
||||
email = strings.ToLower(email)
|
||||
for _, keyword := range keywords {
|
||||
if strings.Contains(email, keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func printSummary(recommendations CleanupRecommendations) {
|
||||
fmt.Printf("\n=== INBOX CLEANUP RECOMMENDATIONS ===\n\n")
|
||||
|
||||
stats := recommendations.StorageStats
|
||||
fmt.Printf("📊 CURRENT STATE:\n")
|
||||
fmt.Printf(" Total emails: %d\n", stats.TotalEmails)
|
||||
fmt.Printf(" Potential for deletion: %d emails\n", stats.DeletionPotential)
|
||||
fmt.Printf(" Potential for archiving: %d emails\n", stats.ArchivePotential)
|
||||
fmt.Printf(" Total inbox reduction: %.1f%%\n\n", stats.PercentageReduction)
|
||||
|
||||
fmt.Printf("🗑️ SAFE TO DELETE (%d emails):\n", stats.DeletionPotential)
|
||||
for i, rec := range recommendations.SafeToDelete {
|
||||
fmt.Printf(" %d. %s (%d emails, %s risk)\n", i+1, rec.Category, rec.Count, rec.Risk)
|
||||
fmt.Printf(" Query: %s\n", rec.Query)
|
||||
}
|
||||
|
||||
fmt.Printf("\n📦 BULK ARCHIVE (%d emails):\n", stats.ArchivePotential)
|
||||
for i, rec := range recommendations.BulkArchive {
|
||||
fmt.Printf(" %d. %s (%d emails)\n", i+1, rec.Category, rec.Count)
|
||||
fmt.Printf(" Query: %s\n", rec.Query)
|
||||
}
|
||||
|
||||
fmt.Printf("\n✋ UNSUBSCRIBE TARGETS:\n")
|
||||
for i, target := range recommendations.UnsubscribeTargets {
|
||||
if i >= 5 {
|
||||
fmt.Printf(" ... and %d more (use 'detailed' output for full list)\n", len(recommendations.UnsubscribeTargets)-5)
|
||||
break
|
||||
}
|
||||
fmt.Printf(" %d. %s (%d emails, %s priority)\n", i+1, target.Sender, target.Count, target.Priority)
|
||||
}
|
||||
|
||||
fmt.Printf("\n⚠️ NEEDS ATTENTION:\n")
|
||||
for i, item := range recommendations.AttentionNeeded {
|
||||
fmt.Printf(" %d. %s\n", i+1, item.Description)
|
||||
fmt.Printf(" Action: %s\n", item.Action)
|
||||
}
|
||||
|
||||
fmt.Printf("\n💡 NEXT STEPS:\n")
|
||||
fmt.Printf(" 1. Review deletion candidates (start with low-risk items)\n")
|
||||
fmt.Printf(" 2. Set up bulk archive operations\n")
|
||||
fmt.Printf(" 3. Unsubscribe from high-priority senders\n")
|
||||
fmt.Printf(" 4. Create Gmail filters to prevent future buildup\n")
|
||||
fmt.Printf(" 5. Address attention items\n\n")
|
||||
fmt.Printf("Use 'detailed' output format for complete Gmail queries and instructions.\n")
|
||||
}
|
||||
|
||||
func printDetailed(recommendations CleanupRecommendations) {
|
||||
printSummary(recommendations)
|
||||
|
||||
fmt.Printf("\n=== DETAILED CLEANUP INSTRUCTIONS ===\n\n")
|
||||
|
||||
fmt.Printf("🗑️ DELETION INSTRUCTIONS:\n")
|
||||
for i, rec := range recommendations.SafeToDelete {
|
||||
fmt.Printf("\n%d. %s (%s risk)\n", i+1, rec.Category, rec.Risk)
|
||||
fmt.Printf(" Gmail Query: %s\n", rec.Query)
|
||||
fmt.Printf(" Description: %s\n", rec.Description)
|
||||
fmt.Printf(" Estimated Count: %d emails\n", rec.Count)
|
||||
fmt.Printf(" Examples: %s\n", strings.Join(rec.Examples, ", "))
|
||||
fmt.Printf(" Instructions:\n")
|
||||
fmt.Printf(" 1. Paste query into Gmail search\n")
|
||||
fmt.Printf(" 2. Review a few emails to confirm they're safe to delete\n")
|
||||
fmt.Printf(" 3. Select all → Delete\n")
|
||||
}
|
||||
|
||||
fmt.Printf("\n📦 ARCHIVE INSTRUCTIONS:\n")
|
||||
for i, rec := range recommendations.BulkArchive {
|
||||
fmt.Printf("\n%d. %s\n", i+1, rec.Category)
|
||||
fmt.Printf(" Gmail Query: %s\n", rec.Query)
|
||||
fmt.Printf(" Description: %s\n", rec.Description)
|
||||
fmt.Printf(" Reason: %s\n", rec.Reason)
|
||||
fmt.Printf(" Estimated Count: %d emails\n", rec.Count)
|
||||
fmt.Printf(" Instructions:\n")
|
||||
fmt.Printf(" 1. Paste query into Gmail search\n")
|
||||
fmt.Printf(" 2. Select all → Archive\n")
|
||||
}
|
||||
|
||||
fmt.Printf("\n✋ UNSUBSCRIBE DETAILS:\n")
|
||||
for i, target := range recommendations.UnsubscribeTargets {
|
||||
fmt.Printf("\n%d. %s (%s priority)\n", i+1, target.Sender, target.Priority)
|
||||
fmt.Printf(" Email Count: %d\n", target.Count)
|
||||
fmt.Printf(" Category: %s\n", target.Category)
|
||||
fmt.Printf(" Reason: %s\n", target.Reason)
|
||||
fmt.Printf(" Gmail Query: from:%s\n", target.Sender)
|
||||
}
|
||||
}
|
||||
|
||||
func outputJSON(recommendations CleanupRecommendations) {
|
||||
encoder := json.NewEncoder(os.Stdout)
|
||||
encoder.SetIndent("", " ")
|
||||
encoder.Encode(recommendations)
|
||||
}
|
||||
356
filters.go
Normal file
356
filters.go
Normal file
@@ -0,0 +1,356 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type GmailFilter struct {
|
||||
Name string `json:"name"`
|
||||
Criteria string `json:"criteria"`
|
||||
Actions string `json:"actions"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type FilterSuggestions struct {
|
||||
Filters []GmailFilter `json:"filters"`
|
||||
UnsubscribeCandidates []string `json:"unsubscribe_candidates"`
|
||||
ArchiveCandidates []string `json:"archive_candidates"`
|
||||
SearchQueries []SearchQuery `json:"search_queries"`
|
||||
}
|
||||
|
||||
type SearchQuery struct {
|
||||
Purpose string `json:"purpose"`
|
||||
Query string `json:"query"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("Usage: go run filters.go <analysis_json_file> [output_format]")
|
||||
fmt.Println(" analysis_json_file: JSON output from analyze.go")
|
||||
fmt.Println(" output_format: filters (default), queries, or all")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
jsonFile := os.Args[1]
|
||||
outputFormat := "filters"
|
||||
if len(os.Args) > 2 {
|
||||
outputFormat = strings.ToLower(os.Args[2])
|
||||
}
|
||||
|
||||
stats, err := loadAnalysisFromJSON(jsonFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading analysis JSON: %v", err)
|
||||
}
|
||||
|
||||
suggestions := generateFilterSuggestions(stats)
|
||||
|
||||
switch outputFormat {
|
||||
case "filters":
|
||||
printFilters(suggestions.Filters)
|
||||
case "queries":
|
||||
printSearchQueries(suggestions.SearchQueries)
|
||||
case "all":
|
||||
printAllSuggestions(suggestions)
|
||||
case "json":
|
||||
outputJSON(suggestions)
|
||||
default:
|
||||
fmt.Printf("Unknown output format: %s\n", outputFormat)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func loadAnalysisFromJSON(filename string) (EmailStats, error) {
|
||||
var stats EmailStats
|
||||
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return stats, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
decoder := json.NewDecoder(file)
|
||||
err = decoder.Decode(&stats)
|
||||
return stats, err
|
||||
}
|
||||
|
||||
func generateFilterSuggestions(stats EmailStats) FilterSuggestions {
|
||||
suggestions := FilterSuggestions{
|
||||
Filters: []GmailFilter{},
|
||||
UnsubscribeCandidates: []string{},
|
||||
ArchiveCandidates: []string{},
|
||||
SearchQueries: []SearchQuery{},
|
||||
}
|
||||
|
||||
// Generate filters based on top domains
|
||||
suggestions.Filters = append(suggestions.Filters, generateDomainFilters(stats.TopDomains)...)
|
||||
|
||||
// Generate filters based on subject patterns
|
||||
suggestions.Filters = append(suggestions.Filters, generatePatternFilters(stats.SubjectPatterns)...)
|
||||
|
||||
// Generate filters based on categories
|
||||
suggestions.Filters = append(suggestions.Filters, generateCategoryFilters(stats.Categories)...)
|
||||
|
||||
// Generate unsubscribe candidates
|
||||
suggestions.UnsubscribeCandidates = generateUnsubscribeCandidates(stats.TopSenders, stats.TopDomains)
|
||||
|
||||
// Generate archive candidates
|
||||
suggestions.ArchiveCandidates = generateArchiveCandidates(stats.TopDomains)
|
||||
|
||||
// Generate search queries
|
||||
suggestions.SearchQueries = generateSearchQueries(stats)
|
||||
|
||||
return suggestions
|
||||
}
|
||||
|
||||
func generateDomainFilters(domains []DomainInfo) []GmailFilter {
|
||||
var filters []GmailFilter
|
||||
|
||||
for _, domain := range domains {
|
||||
if domain.Count < 10 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch domain.Type {
|
||||
case "social":
|
||||
filters = append(filters, GmailFilter{
|
||||
Name: fmt.Sprintf("Auto-label %s", domain.Domain),
|
||||
Criteria: fmt.Sprintf("from:@%s", domain.Domain),
|
||||
Actions: "Apply label: Social, Skip inbox",
|
||||
Description: fmt.Sprintf("Auto-process social media emails from %s (%d emails)", domain.Domain, domain.Count),
|
||||
})
|
||||
case "commerce":
|
||||
filters = append(filters, GmailFilter{
|
||||
Name: fmt.Sprintf("Auto-label %s", domain.Domain),
|
||||
Criteria: fmt.Sprintf("from:@%s", domain.Domain),
|
||||
Actions: "Apply label: Shopping, Skip inbox",
|
||||
Description: fmt.Sprintf("Auto-process shopping emails from %s (%d emails)", domain.Domain, domain.Count),
|
||||
})
|
||||
case "automated":
|
||||
filters = append(filters, GmailFilter{
|
||||
Name: fmt.Sprintf("Auto-archive %s", domain.Domain),
|
||||
Criteria: fmt.Sprintf("from:@%s", domain.Domain),
|
||||
Actions: "Apply label: Automated, Skip inbox, Mark as read",
|
||||
Description: fmt.Sprintf("Auto-archive automated emails from %s (%d emails)", domain.Domain, domain.Count),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return filters
|
||||
}
|
||||
|
||||
func generatePatternFilters(patterns []PatternInfo) []GmailFilter {
|
||||
var filters []GmailFilter
|
||||
|
||||
for _, pattern := range patterns {
|
||||
if pattern.Count < 5 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch pattern.Pattern {
|
||||
case "newsletter":
|
||||
filters = append(filters, GmailFilter{
|
||||
Name: "Auto-label newsletters",
|
||||
Criteria: `subject:(newsletter OR weekly OR monthly OR digest)`,
|
||||
Actions: "Apply label: Newsletters, Skip inbox",
|
||||
Description: fmt.Sprintf("Auto-process newsletter emails (%d found)", pattern.Count),
|
||||
})
|
||||
case "promotional":
|
||||
filters = append(filters, GmailFilter{
|
||||
Name: "Auto-label promotions",
|
||||
Criteria: `subject:(sale OR deal OR offer OR discount OR "%" OR free)`,
|
||||
Actions: "Apply label: Promotions, Skip inbox",
|
||||
Description: fmt.Sprintf("Auto-process promotional emails (%d found)", pattern.Count),
|
||||
})
|
||||
case "notification":
|
||||
filters = append(filters, GmailFilter{
|
||||
Name: "Auto-label notifications",
|
||||
Criteria: `subject:(notification OR alert OR reminder)`,
|
||||
Actions: "Apply label: Notifications",
|
||||
Description: fmt.Sprintf("Auto-label notification emails (%d found)", pattern.Count),
|
||||
})
|
||||
case "transactional":
|
||||
filters = append(filters, GmailFilter{
|
||||
Name: "Auto-label receipts",
|
||||
Criteria: `subject:(receipt OR confirmation OR invoice OR payment)`,
|
||||
Actions: "Apply label: Receipts, Star it",
|
||||
Description: fmt.Sprintf("Auto-label important transactional emails (%d found)", pattern.Count),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return filters
|
||||
}
|
||||
|
||||
func generateCategoryFilters(categories map[string]int) []GmailFilter {
|
||||
var filters []GmailFilter
|
||||
|
||||
for category, count := range categories {
|
||||
if count < 20 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch category {
|
||||
case "newsletters":
|
||||
filters = append(filters, GmailFilter{
|
||||
Name: "Auto-handle newsletters",
|
||||
Criteria: `from:(noreply OR no-reply) OR subject:unsubscribe`,
|
||||
Actions: "Apply label: Newsletters, Skip inbox",
|
||||
Description: fmt.Sprintf("Auto-process newsletter-type emails (%d found)", count),
|
||||
})
|
||||
case "work":
|
||||
filters = append(filters, GmailFilter{
|
||||
Name: "Auto-label work tools",
|
||||
Criteria: `from:(slack.com OR github.com OR jira OR atlassian)`,
|
||||
Actions: "Apply label: Work",
|
||||
Description: fmt.Sprintf("Auto-label work-related tool emails (%d found)", count),
|
||||
})
|
||||
case "finance":
|
||||
filters = append(filters, GmailFilter{
|
||||
Name: "Auto-label finance",
|
||||
Criteria: `subject:(payment OR invoice OR receipt) OR from:(paypal OR stripe OR bank)`,
|
||||
Actions: "Apply label: Finance, Star it",
|
||||
Description: fmt.Sprintf("Auto-label financial emails (%d found)", count),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return filters
|
||||
}
|
||||
|
||||
func generateUnsubscribeCandidates(senders []SenderInfo, domains []DomainInfo) []string {
|
||||
var candidates []string
|
||||
|
||||
// High-volume newsletter senders
|
||||
for _, sender := range senders {
|
||||
if sender.Count > 20 && (strings.Contains(strings.ToLower(sender.Email), "noreply") ||
|
||||
strings.Contains(strings.ToLower(sender.Email), "no-reply") ||
|
||||
strings.Contains(strings.ToLower(sender.Email), "newsletter")) {
|
||||
candidates = append(candidates, fmt.Sprintf("%s (%d emails)", sender.Email, sender.Count))
|
||||
}
|
||||
}
|
||||
|
||||
// High-volume automated domains
|
||||
for _, domain := range domains {
|
||||
if domain.Count > 30 && domain.Type == "automated" {
|
||||
candidates = append(candidates, fmt.Sprintf("All emails from @%s (%d emails)", domain.Domain, domain.Count))
|
||||
}
|
||||
}
|
||||
|
||||
return candidates
|
||||
}
|
||||
|
||||
func generateArchiveCandidates(domains []DomainInfo) []string {
|
||||
var candidates []string
|
||||
|
||||
for _, domain := range domains {
|
||||
if domain.Count > 50 && (domain.Type == "automated" || domain.Type == "social") {
|
||||
candidates = append(candidates, fmt.Sprintf("@%s (%d emails, %s)", domain.Domain, domain.Count, domain.Type))
|
||||
}
|
||||
}
|
||||
|
||||
return candidates
|
||||
}
|
||||
|
||||
func generateSearchQueries(stats EmailStats) []SearchQuery {
|
||||
queries := []SearchQuery{
|
||||
{
|
||||
Purpose: "Find old newsletters",
|
||||
Query: `subject:(newsletter OR weekly OR monthly) older_than:6m`,
|
||||
Description: "Find newsletters older than 6 months that can be safely deleted",
|
||||
},
|
||||
{
|
||||
Purpose: "Find promotional emails",
|
||||
Query: `subject:(sale OR deal OR offer OR discount) older_than:3m`,
|
||||
Description: "Find promotional emails older than 3 months",
|
||||
},
|
||||
{
|
||||
Purpose: "Find social notifications",
|
||||
Query: `from:(facebook OR twitter OR linkedin OR instagram) older_than:1m`,
|
||||
Description: "Find social media notifications older than 1 month",
|
||||
},
|
||||
{
|
||||
Purpose: "Find automated emails",
|
||||
Query: `from:(noreply OR no-reply) older_than:1y`,
|
||||
Description: "Find automated emails older than 1 year",
|
||||
},
|
||||
{
|
||||
Purpose: "Find large attachments",
|
||||
Query: `has:attachment larger:10M`,
|
||||
Description: "Find emails with attachments larger than 10MB",
|
||||
},
|
||||
{
|
||||
Purpose: "Find unread old emails",
|
||||
Query: `is:unread older_than:1m`,
|
||||
Description: "Find unread emails older than 1 month that might need attention",
|
||||
},
|
||||
}
|
||||
|
||||
// Add domain-specific queries for top domains
|
||||
for i, domain := range stats.TopDomains {
|
||||
if i >= 3 || domain.Count < 50 {
|
||||
break
|
||||
}
|
||||
queries = append(queries, SearchQuery{
|
||||
Purpose: fmt.Sprintf("Review %s emails", domain.Domain),
|
||||
Query: fmt.Sprintf(`from:@%s`, domain.Domain),
|
||||
Description: fmt.Sprintf("Review all %d emails from %s", domain.Count, domain.Domain),
|
||||
})
|
||||
}
|
||||
|
||||
return queries
|
||||
}
|
||||
|
||||
func printFilters(filters []GmailFilter) {
|
||||
fmt.Printf("\n=== GMAIL FILTER SUGGESTIONS ===\n\n")
|
||||
fmt.Printf("To create these filters in Gmail:\n")
|
||||
fmt.Printf("1. Go to Gmail Settings > Filters and Blocked Addresses\n")
|
||||
fmt.Printf("2. Click 'Create a new filter'\n")
|
||||
fmt.Printf("3. Use the criteria below, then apply the suggested actions\n\n")
|
||||
|
||||
for i, filter := range filters {
|
||||
fmt.Printf("%d. %s\n", i+1, filter.Name)
|
||||
fmt.Printf(" Criteria: %s\n", filter.Criteria)
|
||||
fmt.Printf(" Actions: %s\n", filter.Actions)
|
||||
fmt.Printf(" Description: %s\n\n", filter.Description)
|
||||
}
|
||||
}
|
||||
|
||||
func printSearchQueries(queries []SearchQuery) {
|
||||
fmt.Printf("\n=== GMAIL SEARCH QUERIES ===\n\n")
|
||||
fmt.Printf("Copy and paste these queries into Gmail's search box:\n\n")
|
||||
|
||||
for i, query := range queries {
|
||||
fmt.Printf("%d. %s\n", i+1, query.Purpose)
|
||||
fmt.Printf(" Query: %s\n", query.Query)
|
||||
fmt.Printf(" Description: %s\n\n", query.Description)
|
||||
}
|
||||
}
|
||||
|
||||
func printAllSuggestions(suggestions FilterSuggestions) {
|
||||
printFilters(suggestions.Filters)
|
||||
|
||||
fmt.Printf("\n=== UNSUBSCRIBE CANDIDATES ===\n")
|
||||
fmt.Printf("Consider unsubscribing from these high-volume senders:\n\n")
|
||||
for i, candidate := range suggestions.UnsubscribeCandidates {
|
||||
fmt.Printf("%d. %s\n", i+1, candidate)
|
||||
}
|
||||
|
||||
fmt.Printf("\n=== BULK ARCHIVE CANDIDATES ===\n")
|
||||
fmt.Printf("Consider bulk archiving emails from these domains:\n\n")
|
||||
for i, candidate := range suggestions.ArchiveCandidates {
|
||||
fmt.Printf("%d. %s\n", i+1, candidate)
|
||||
}
|
||||
|
||||
printSearchQueries(suggestions.SearchQueries)
|
||||
}
|
||||
|
||||
func outputJSON(suggestions FilterSuggestions) {
|
||||
encoder := json.NewEncoder(os.Stdout)
|
||||
encoder.SetIndent("", " ")
|
||||
encoder.Encode(suggestions)
|
||||
}
|
||||
28
go.mod
Normal file
28
go.mod
Normal file
@@ -0,0 +1,28 @@
|
||||
module mail-automation
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
golang.org/x/oauth2 v0.15.0
|
||||
google.golang.org/api v0.153.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.23.3 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/google/uuid v1.4.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/crypto v0.16.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
)
|
||||
143
go.sum
Normal file
143
go.sum
Normal file
@@ -0,0 +1,143 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
|
||||
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
|
||||
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.153.0 h1:N1AwGhielyKFaUqH07/ZSIQR3uNPcV7NVw0vj+j4iR4=
|
||||
google.golang.org/api v0.153.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
|
||||
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
173
main.go
Normal file
173
main.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
"google.golang.org/api/gmail/v1"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
type EmailInfo struct {
|
||||
Sender string `json:"sender"`
|
||||
Subject string `json:"subject"`
|
||||
Date string `json:"date"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 4 {
|
||||
fmt.Println("Usage: go run main.go <client_id> <client_secret> <output_format>")
|
||||
fmt.Println(" client_id: Your Google OAuth2 client ID")
|
||||
fmt.Println(" client_secret: Your Google OAuth2 client secret")
|
||||
fmt.Println(" output_format: csv or json")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
clientID := os.Args[1]
|
||||
clientSecret := os.Args[2]
|
||||
outputFormat := strings.ToLower(os.Args[3])
|
||||
|
||||
if outputFormat != "csv" && outputFormat != "json" {
|
||||
log.Fatal("Output format must be 'csv' or 'json'")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
config := &oauth2.Config{
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
RedirectURL: "urn:ietf:wg:oauth:2.0:oob",
|
||||
Scopes: []string{gmail.GmailReadonlyScope},
|
||||
Endpoint: google.Endpoint,
|
||||
}
|
||||
|
||||
client := getClient(config)
|
||||
|
||||
srv, err := gmail.NewService(ctx, option.WithHTTPClient(client))
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to retrieve Gmail client: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Connected to Gmail API successfully\n")
|
||||
fmt.Printf("Fetching emails from inbox...\n")
|
||||
|
||||
emails := fetchEmails(srv)
|
||||
|
||||
fmt.Printf("Generating %s output...\n", outputFormat)
|
||||
|
||||
switch outputFormat {
|
||||
case "csv":
|
||||
outputCSV(emails)
|
||||
case "json":
|
||||
outputJSON(emails)
|
||||
}
|
||||
|
||||
fmt.Printf("Complete! Fetched %d emails\n", len(emails))
|
||||
}
|
||||
|
||||
func getClient(config *oauth2.Config) *http.Client {
|
||||
authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
|
||||
fmt.Printf("Visit this URL in your browser: %v\n\n", authURL)
|
||||
fmt.Print("Paste the authorization code here: ")
|
||||
|
||||
var authCode string
|
||||
if _, err := fmt.Scanln(&authCode); err != nil {
|
||||
log.Fatalf("Unable to read authorization code: %v", err)
|
||||
}
|
||||
|
||||
tok, err := config.Exchange(context.TODO(), authCode)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to retrieve token: %v", err)
|
||||
}
|
||||
|
||||
return config.Client(context.Background(), tok)
|
||||
}
|
||||
|
||||
func fetchEmails(srv *gmail.Service) []EmailInfo {
|
||||
var emails []EmailInfo
|
||||
pageCount := 0
|
||||
|
||||
req := srv.Users.Messages.List("me").Q("in:inbox")
|
||||
|
||||
for {
|
||||
pageCount++
|
||||
fmt.Printf("Fetching page %d...\n", pageCount)
|
||||
|
||||
r, err := req.Do()
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to retrieve messages: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Processing %d messages from page %d...\n", len(r.Messages), pageCount)
|
||||
|
||||
for i, m := range r.Messages {
|
||||
if (i+1)%50 == 0 {
|
||||
fmt.Printf(" Processed %d/%d messages on this page\n", i+1, len(r.Messages))
|
||||
}
|
||||
|
||||
msg, err := srv.Users.Messages.Get("me", m.Id).Do()
|
||||
if err != nil {
|
||||
log.Printf("Unable to retrieve message %s: %v", m.Id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
email := parseMessage(msg)
|
||||
emails = append(emails, email)
|
||||
}
|
||||
|
||||
fmt.Printf("Completed page %d. Total emails collected: %d\n", pageCount, len(emails))
|
||||
|
||||
if r.NextPageToken == "" {
|
||||
fmt.Printf("Reached end of inbox. Processing complete.\n")
|
||||
break
|
||||
}
|
||||
req.PageToken(r.NextPageToken)
|
||||
}
|
||||
|
||||
return emails
|
||||
}
|
||||
|
||||
func parseMessage(msg *gmail.Message) EmailInfo {
|
||||
email := EmailInfo{
|
||||
ID: msg.Id,
|
||||
}
|
||||
|
||||
for _, header := range msg.Payload.Headers {
|
||||
switch header.Name {
|
||||
case "From":
|
||||
email.Sender = header.Value
|
||||
case "Subject":
|
||||
email.Subject = header.Value
|
||||
case "Date":
|
||||
email.Date = header.Value
|
||||
}
|
||||
}
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
func outputCSV(emails []EmailInfo) {
|
||||
writer := csv.NewWriter(os.Stdout)
|
||||
defer writer.Flush()
|
||||
|
||||
writer.Write([]string{"Sender", "Subject", "Date", "ID"})
|
||||
|
||||
for _, email := range emails {
|
||||
writer.Write([]string{email.Sender, email.Subject, email.Date, email.ID})
|
||||
}
|
||||
}
|
||||
|
||||
func outputJSON(emails []EmailInfo) {
|
||||
encoder := json.NewEncoder(os.Stdout)
|
||||
encoder.SetIndent("", " ")
|
||||
encoder.Encode(emails)
|
||||
}
|
||||
241
organize.go
Normal file
241
organize.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type OrganizationStep struct {
|
||||
Name string
|
||||
Description string
|
||||
Command string
|
||||
Args []string
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("Usage: go run organize.go <csv_file>")
|
||||
fmt.Println(" csv_file: path to CSV file from main.go")
|
||||
fmt.Println("\nThis interactive tool will help you organize your Gmail inbox step by step.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
csvFile := os.Args[1]
|
||||
|
||||
fmt.Printf("\n🎯 GMAIL INBOX ORGANIZATION WIZARD\n")
|
||||
fmt.Printf("===================================\n\n")
|
||||
|
||||
fmt.Printf("This tool will guide you through organizing your Gmail inbox using the analysis\n")
|
||||
fmt.Printf("from your exported email data (%s).\n\n", csvFile)
|
||||
|
||||
// Step 1: Run analysis
|
||||
fmt.Printf("STEP 1: Analyzing your emails...\n")
|
||||
analysisFile := runAnalysis(csvFile)
|
||||
|
||||
// Step 2: Show summary and get user preference
|
||||
fmt.Printf("\nSTEP 2: Choose your organization approach...\n")
|
||||
approach := getUserApproach()
|
||||
|
||||
// Step 3: Execute organization plan
|
||||
fmt.Printf("\nSTEP 3: Executing organization plan...\n")
|
||||
executeOrganizationPlan(analysisFile, approach)
|
||||
|
||||
fmt.Printf("\n✅ Organization complete!\n")
|
||||
fmt.Printf("\nNext steps:\n")
|
||||
fmt.Printf("1. Review the generated Gmail filters and search queries\n")
|
||||
fmt.Printf("2. Apply them gradually to your Gmail account\n")
|
||||
fmt.Printf("3. Monitor the results and adjust as needed\n")
|
||||
fmt.Printf("4. Run this tool periodically to maintain organization\n")
|
||||
}
|
||||
|
||||
func runAnalysis(csvFile string) string {
|
||||
fmt.Printf("Running email analysis...\n")
|
||||
|
||||
analysisFile := strings.TrimSuffix(csvFile, ".csv") + "_analysis.json"
|
||||
|
||||
cmd := exec.Command("go", "run", "analyze.go", csvFile, "json")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.Fatalf("Error running analysis: %v", err)
|
||||
}
|
||||
|
||||
// Save analysis to file
|
||||
err = os.WriteFile(analysisFile, output, 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("Error saving analysis: %v", err)
|
||||
}
|
||||
|
||||
// Also show summary
|
||||
cmd = exec.Command("go", "run", "analyze.go", csvFile, "summary")
|
||||
output, err = cmd.Output()
|
||||
if err != nil {
|
||||
log.Printf("Warning: Could not generate summary: %v", err)
|
||||
} else {
|
||||
fmt.Printf("%s\n", output)
|
||||
}
|
||||
|
||||
return analysisFile
|
||||
}
|
||||
|
||||
func getUserApproach() string {
|
||||
fmt.Printf("\nChoose your organization approach:\n\n")
|
||||
fmt.Printf("1. 🧹 AGGRESSIVE CLEANUP\n")
|
||||
fmt.Printf(" - Focus on deleting and archiving old emails\n")
|
||||
fmt.Printf(" - Best for: Very cluttered inboxes, storage concerns\n")
|
||||
fmt.Printf(" - Risk: Moderate (might delete emails you want to keep)\n\n")
|
||||
|
||||
fmt.Printf("2. 📋 FILTER-FOCUSED\n")
|
||||
fmt.Printf(" - Focus on creating Gmail filters for future organization\n")
|
||||
fmt.Printf(" - Best for: Maintaining organization going forward\n")
|
||||
fmt.Printf(" - Risk: Low (doesn't delete anything)\n\n")
|
||||
|
||||
fmt.Printf("3. 🎯 BALANCED APPROACH\n")
|
||||
fmt.Printf(" - Combination of cleanup and filters\n")
|
||||
fmt.Printf(" - Best for: Most users\n")
|
||||
fmt.Printf(" - Risk: Low to moderate\n\n")
|
||||
|
||||
fmt.Printf("4. 📊 ANALYSIS ONLY\n")
|
||||
fmt.Printf(" - Just generate reports and recommendations\n")
|
||||
fmt.Printf(" - Best for: Understanding your email patterns first\n")
|
||||
fmt.Printf(" - Risk: None\n\n")
|
||||
|
||||
for {
|
||||
fmt.Printf("Enter your choice (1-4): ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
input, _ := reader.ReadString('\n')
|
||||
choice := strings.TrimSpace(input)
|
||||
|
||||
switch choice {
|
||||
case "1":
|
||||
return "aggressive"
|
||||
case "2":
|
||||
return "filters"
|
||||
case "3":
|
||||
return "balanced"
|
||||
case "4":
|
||||
return "analysis"
|
||||
default:
|
||||
fmt.Printf("Invalid choice. Please enter 1, 2, 3, or 4.\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func executeOrganizationPlan(analysisFile, approach string) {
|
||||
switch approach {
|
||||
case "aggressive":
|
||||
executeAggressiveCleanup(analysisFile)
|
||||
case "filters":
|
||||
executeFilterFocus(analysisFile)
|
||||
case "balanced":
|
||||
executeBalancedApproach(analysisFile)
|
||||
case "analysis":
|
||||
executeAnalysisOnly(analysisFile)
|
||||
}
|
||||
}
|
||||
|
||||
func executeAggressiveCleanup(analysisFile string) {
|
||||
fmt.Printf("\n🧹 AGGRESSIVE CLEANUP APPROACH\n")
|
||||
fmt.Printf("==============================\n\n")
|
||||
|
||||
// Generate cleanup recommendations
|
||||
fmt.Printf("Generating cleanup recommendations...\n")
|
||||
runCommand("go", "run", "cleanup.go", analysisFile, "detailed")
|
||||
|
||||
fmt.Printf("\n⚠️ IMPORTANT:\n")
|
||||
fmt.Printf("1. Review the deletion recommendations carefully\n")
|
||||
fmt.Printf("2. Start with 'Low risk' deletions first\n")
|
||||
fmt.Printf("3. Test on a small batch before doing bulk operations\n")
|
||||
fmt.Printf("4. Consider creating a backup label before deleting\n")
|
||||
|
||||
// Also generate filters for future
|
||||
fmt.Printf("\nGenerating filters to prevent future buildup...\n")
|
||||
runCommand("go", "run", "filters.go", analysisFile, "filters")
|
||||
}
|
||||
|
||||
func executeFilterFocus(analysisFile string) {
|
||||
fmt.Printf("\n📋 FILTER-FOCUSED APPROACH\n")
|
||||
fmt.Printf("==========================\n\n")
|
||||
|
||||
fmt.Printf("Generating Gmail filter recommendations...\n")
|
||||
runCommand("go", "run", "filters.go", analysisFile, "all")
|
||||
|
||||
fmt.Printf("\n💡 IMPLEMENTATION TIPS:\n")
|
||||
fmt.Printf("1. Start with high-impact filters (newsletters, social media)\n")
|
||||
fmt.Printf("2. Test each filter on a small subset first\n")
|
||||
fmt.Printf("3. Create labels before creating filters\n")
|
||||
fmt.Printf("4. Use 'Skip inbox' for non-urgent categories\n")
|
||||
}
|
||||
|
||||
func executeBalancedApproach(analysisFile string) {
|
||||
fmt.Printf("\n🎯 BALANCED APPROACH\n")
|
||||
fmt.Printf("====================\n\n")
|
||||
|
||||
// First show cleanup for low-risk items
|
||||
fmt.Printf("PHASE 1: Safe cleanup recommendations\n")
|
||||
fmt.Printf("------------------------------------\n")
|
||||
runCommand("go", "run", "cleanup.go", analysisFile, "summary")
|
||||
|
||||
fmt.Printf("\nPHASE 2: Gmail filter setup\n")
|
||||
fmt.Printf("---------------------------\n")
|
||||
runCommand("go", "run", "filters.go", analysisFile, "filters")
|
||||
|
||||
fmt.Printf("\n📋 RECOMMENDED ORDER:\n")
|
||||
fmt.Printf("1. Create Gmail filters first (prevents future clutter)\n")
|
||||
fmt.Printf("2. Apply low-risk deletions\n")
|
||||
fmt.Printf("3. Set up bulk archive operations\n")
|
||||
fmt.Printf("4. Review and unsubscribe from high-volume senders\n")
|
||||
}
|
||||
|
||||
func executeAnalysisOnly(analysisFile string) {
|
||||
fmt.Printf("\n📊 ANALYSIS REPORT\n")
|
||||
fmt.Printf("==================\n\n")
|
||||
|
||||
fmt.Printf("Email Analysis Summary:\n")
|
||||
fmt.Printf("----------------------\n")
|
||||
runCommand("go", "run", "analyze.go", strings.Replace(analysisFile, "_analysis.json", ".csv", 1), "summary")
|
||||
|
||||
fmt.Printf("\nDetailed Cleanup Potential:\n")
|
||||
fmt.Printf("--------------------------\n")
|
||||
runCommand("go", "run", "cleanup.go", analysisFile, "summary")
|
||||
|
||||
fmt.Printf("\nGmail Search Queries:\n")
|
||||
fmt.Printf("--------------------\n")
|
||||
runCommand("go", "run", "filters.go", analysisFile, "queries")
|
||||
|
||||
fmt.Printf("\n💡 NEXT STEPS:\n")
|
||||
fmt.Printf("Run this tool again with a different approach when you're ready to take action.\n")
|
||||
}
|
||||
|
||||
func runCommand(name string, args ...string) {
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Command failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func openURL(url string) error {
|
||||
var cmd string
|
||||
var args []string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
cmd = "cmd"
|
||||
args = []string{"/c", "start"}
|
||||
case "darwin":
|
||||
cmd = "open"
|
||||
default: // "linux", "freebsd", "openbsd", "netbsd"
|
||||
cmd = "xdg-open"
|
||||
}
|
||||
args = append(args, url)
|
||||
return exec.Command(cmd, args...).Start()
|
||||
}
|
||||
Reference in New Issue
Block a user