This commit is contained in:
orejav
2025-07-28 03:03:24 +03:00
commit b65c0956d2
10 changed files with 2092 additions and 0 deletions

104
CLAUDE.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()
}

2
secret Normal file
View File

@@ -0,0 +1,2 @@
330651279218-ckbo1g84tkid9ghcm6jdkod3hkkdr6tu.apps.googleusercontent.com
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