package main import ( "database/sql" "flag" "fmt" "log" "net/mail" "os" "path/filepath" "strings" _ "github.com/mattn/go-sqlite3" ) var ( quiet = flag.Bool("q", false, "don't print status messages to stdout") debugLogFn = flag.String("debugLogFn", "/dev/null", "file to store debugging logs") sentDirGlob = flag.String("sentGlob", "${HOME}/Maildir/Sent/*/*", "Pattern for email sent files") addressDbFn = flag.String("addressDbFn", "${HOME}/.mutt/abook.db", "Location to store sqlite database of contacts") ) type Contact struct { mail.Address fn string } func getAddresses(fn string, results chan<- *Contact) { file, err := os.Open(fn) if err != nil { log.Fatalf("Failed to open %q: %s\n", fn, err) } msg, err := mail.ReadMessage(file) if err != nil { log.Printf("Failed to parse message %q: %s\n", fn, err) return } headers := []string{"to", "cc", "resent-to", "resent-cc", "bcc"} for _, header := range headers { addrs, err := msg.Header.AddressList(header) if err != nil { //log.Printf("Failed to parse %s: header in %q: %s", header, fn, err) continue } for _, addr := range addrs { c := &Contact{*addr, fn} results <- c } } } func getEmails(results chan<- *Contact) { files, err := filepath.Glob(os.ExpandEnv(*sentDirGlob)) if !(*quiet) { fmt.Printf("Scanning %d files\n", len(files)) } if err != nil { log.Fatalf("Failed to glob %q: %s\n", *sentDirGlob, err) } for _, fn := range files { getAddresses(fn, results) } close(results) } func createEmailTable(db *sql.DB) { sqls := []string{ "DROP TABLE IF EXISTS emails", "CREATE TABLE emails (email, name, fn)", } for _, sql := range sqls { _, err := db.Exec(sql) if err != nil { log.Fatalf("Error initializing DB %q: %s\n", err, sql) } } } func createSearchTable(db *sql.DB) { sqls := []string{ "DROP TABLE IF EXISTS search", `CREATE TABLE search AS SELECT email, name, count(*) AS frequency FROM emails group BY name, email ORDER BY frequency`, } for _, sql := range sqls { _, err := db.Exec(sql) if err != nil { log.Fatalf("Error creating search table %q: %s\n", err, sql) } } } func fillEmailTable(db *sql.DB, results <-chan *Contact) { tx, err := db.Begin() if err != nil { log.Fatalf("Faild to being transaction: %s\n", err) return } defer tx.Commit() stmt, err := tx.Prepare("INSERT INTO emails VALUES (?, ?, ?)") defer stmt.Close() if err != nil { log.Fatalf("Error preparing statement: %s\n", err) return } count := 1 for c := range results { addr := c.Address if !(*quiet) { if 0 == count%50 { fmt.Print(count) } if 0 == count%10 { fmt.Print(".") } } count += 1 var name string if addr.Name != "" { name = addr.Name if -1 != strings.Index(name, ",") { parts := strings.SplitN(name, ",", 2) first, second := strings.TrimSpace(parts[1]), parts[0] name = fmt.Sprintf("%s %s", first, second) } } else { name = addr.Address } address := strings.ToLower(addr.Address) _, err = stmt.Exec(address, name, c.fn) if err != nil { log.Fatalf("Failed to insert value: %s\n", err) } } } func buildDb(results <-chan *Contact) { os.Remove(os.ExpandEnv(*addressDbFn)) db, err := sql.Open("sqlite3", os.ExpandEnv(*addressDbFn)) if err != nil { fmt.Println(err) return } defer db.Close() createEmailTable(db) fillEmailTable(db, results) createSearchTable(db) } func init() { flag.Parse() } func main() { logFile, err := os.OpenFile(*debugLogFn, os.O_CREATE|os.O_WRONLY, 0644) if err != nil { log.Fatalf("Failed to open log %q: %s\n", *debugLogFn, err) } defer logFile.Close() log.SetOutput(logFile) log.Println("Started") results := make(chan *Contact) go getEmails(results) buildDb(results) log.Println("Finished successfully") }