191 lines
3.7 KiB
Go
191 lines
3.7 KiB
Go
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")
|
|
}
|