From b65c0956d2be9a3bd54cfd25aed230e8943b2038 Mon Sep 17 00:00:00 2001 From: orejav Date: Mon, 28 Jul 2025 03:03:24 +0300 Subject: [PATCH] initiate --- CLAUDE.md | 104 +++++++++++ README.md | 102 +++++++++++ analyze.go | 443 ++++++++++++++++++++++++++++++++++++++++++++++ cleanup.go | 500 ++++++++++++++++++++++++++++++++++++++++++++++++++++ filters.go | 356 +++++++++++++++++++++++++++++++++++++ go.mod | 28 +++ go.sum | 143 +++++++++++++++ main.go | 173 ++++++++++++++++++ organize.go | 241 +++++++++++++++++++++++++ secret | 2 + 10 files changed, 2092 insertions(+) create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 analyze.go create mode 100644 cleanup.go create mode 100644 filters.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 organize.go create mode 100644 secret diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..68cb4ca --- /dev/null +++ b/CLAUDE.md @@ -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 + +# 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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf24f8e --- /dev/null +++ b/README.md @@ -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 +``` + +### 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 \ No newline at end of file diff --git a/analyze.go b/analyze.go new file mode 100644 index 0000000..0aecd6a --- /dev/null +++ b/analyze.go @@ -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 [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 " + 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) +} \ No newline at end of file diff --git a/cleanup.go b/cleanup.go new file mode 100644 index 0000000..cd4479f --- /dev/null +++ b/cleanup.go @@ -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 [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) +} \ No newline at end of file diff --git a/filters.go b/filters.go new file mode 100644 index 0000000..8c58f6d --- /dev/null +++ b/filters.go @@ -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 [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) +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cca4d37 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fbff1b3 --- /dev/null +++ b/go.sum @@ -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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..6b3b137 --- /dev/null +++ b/main.go @@ -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 ") + 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) +} \ No newline at end of file diff --git a/organize.go b/organize.go new file mode 100644 index 0000000..a44e67e --- /dev/null +++ b/organize.go @@ -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 ") + 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() +} \ No newline at end of file diff --git a/secret b/secret new file mode 100644 index 0000000..b273608 --- /dev/null +++ b/secret @@ -0,0 +1,2 @@ +330651279218-ckbo1g84tkid9ghcm6jdkod3hkkdr6tu.apps.googleusercontent.com +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