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