package main // stdlib imports import "os" import "path" import "log" import "fmt" import "regexp" import "strings" import "sort" // 3rd-party imports import "notmuch" import "github.com/kless/goconfig/config" type mail_addr_freq struct { addr string count [3]uint } type frequencies map[string]uint /* Used to sort the email addresses from most to least used */ func sort_by_freq(m1, m2 *mail_addr_freq) int { if (m1.count[0] == m2.count[0] && m1.count[1] == m2.count[1] && m1.count[2] == m2.count[2]) { return 0 } if (m1.count[0] > m2.count[0] || m1.count[0] == m2.count[0] && m1.count[1] > m2.count[1] || m1.count[0] == m2.count[0] && m1.count[1] == m2.count[1] && m1.count[2] > m2.count[2]) { return -1 } return 1 } type maddresses []*mail_addr_freq func (self *maddresses) Len() int { return len(*self) } func (self *maddresses) Less(i,j int) bool { m1 := (*self)[i] m2 := (*self)[j] v := sort_by_freq(m1, m2) if v<=0 { return true } return false } func (self *maddresses) Swap(i,j int) { (*self)[i], (*self)[j] = (*self)[j], (*self)[i] } // find most frequent real name for each mail address func frequent_fullname(freqs frequencies) string { var maxfreq uint = 0 fullname := "" freqs_sz := len(freqs) for mail,freq := range freqs { if (freq > maxfreq && mail != "") || freqs_sz == 1 { // only use the entry if it has a real name // or if this is the only entry maxfreq = freq fullname = mail } } return fullname } func addresses_by_frequency(msgs *notmuch.Messages, name string, pass uint, addr_to_realname *map[string]*frequencies) *frequencies { freqs := make(frequencies) pattern := `\s*(("(\.|[^"])*"|[^,])*?)` // pattern := "\\s*((\\\"(\\\\.|[^\\\\\"])*\\\"|[^,])*" + // "\\b\\w+([-+.]\\w+)*\\@\\w+[-\\.\\w]*\\.([-\\.\\w]+)*\\w\\b)>?)" pattern = `.*` + strings.ToLower(name) + `.*` var re *regexp.Regexp = nil var err os.Error = nil if re,err = regexp.Compile(pattern); err != nil { log.Printf("error: %v\n", err) return &freqs } headers := []string{"from"} if pass == 1 { headers = append(headers, "to", "cc", "bcc") } for ;msgs.Valid();msgs.MoveToNext() { msg := msgs.Get() //println("==> msg [", msg.GetMessageId(), "]") for _,header := range headers { froms := strings.ToLower(msg.GetHeader(header)) //println(" froms: ["+froms+"]") for _,from := range strings.Split(froms, ",", -1) { from = strings.Trim(from, " ") match := re.FindString(from) //println(" -> match: ["+match+"]") occ,ok := freqs[match] if !ok { freqs[match] = 0 occ = 0 } freqs[match] = occ+1 } } } return &freqs } func search_address_passes(queries [3]*notmuch.Query, name string) []string { var val []string addr_freq := make(map[string]*mail_addr_freq) addr_to_realname := make(map[string]*frequencies) var pass uint = 0 // 0-based for _,query := range queries { if query == nil { //println("**warning: idx [",idx,"] contains a nil query") continue } msgs := query.SearchMessages() ht := addresses_by_frequency(msgs, name, pass, &addr_to_realname) for addr, count := range *ht { freq,ok := addr_freq[addr] if !ok { freq = &mail_addr_freq{addr:addr, count:[3]uint{0,0,0}} } freq.count[pass] = count addr_freq[addr] = freq } msgs.Destroy() pass += 1 } addrs := make(maddresses, len(addr_freq)) { iaddr := 0 for _, freq := range addr_freq { addrs[iaddr] = freq iaddr += 1 } } sort.Sort(&addrs) for _,addr := range addrs { freqs,ok := addr_to_realname[addr.addr] if ok { val = append(val, frequent_fullname(*freqs)) } else { val = append(val, addr.addr) } } //println("val:",val) return val } type address_matcher struct { // the notmuch database db *notmuch.Database // full path of the notmuch database user_db_path string // user primary email user_primary_email string // user tag to mark from addresses as in the address book user_addrbook_tag string } func new_address_matcher() *address_matcher { var cfg *config.Config var err os.Error // honor NOTMUCH_CONFIG home := os.Getenv("NOTMUCH_CONFIG") if home == "" { home = os.Getenv("HOME") } if cfg,err = config.ReadDefault(path.Join(home, ".notmuch-config")); err != nil { log.Fatalf("error loading config file:",err) } db_path,_ := cfg.String("database", "path") primary_email,_ := cfg.String("user", "primary_email") addrbook_tag,err := cfg.String("user", "addrbook_tag") if err != nil { addrbook_tag = "addressbook" } self := &address_matcher{db:nil, user_db_path:db_path, user_primary_email:primary_email, user_addrbook_tag:addrbook_tag} return self } func (self *address_matcher) run(name string) { queries := [3]*notmuch.Query{} // open the database if db, status := notmuch.OpenDatabase(self.user_db_path, notmuch.DATABASE_MODE_READ_ONLY); status == notmuch.STATUS_SUCCESS { self.db = db } else { log.Fatalf("Failed to open the database: %v\n", status) } // pass 1: look at all from: addresses with the address book tag query := "tag:" + self.user_addrbook_tag if name != "" { query = query + " and from:" + name + "*" } queries[0] = self.db.CreateQuery(query) // pass 2: look at all to: addresses sent from our primary mail query = "" if name != "" { query = "to:"+name+"*" } if self.user_primary_email != "" { query = query + " from:" + self.user_primary_email } queries[1] = self.db.CreateQuery(query) // if that leads only to a few hits, we check every from too if queries[0].CountMessages() + queries[1].CountMessages() < 10 { query = "" if name != "" { query = "from:"+name+"*" } queries[2] = self.db.CreateQuery(query) } // actually retrieve and sort addresses results := search_address_passes(queries, name) for _,v := range results { if v != "" && v != "\n" { fmt.Println(v) } } return } func main() { //fmt.Println("args:",os.Args) app := new_address_matcher() name := "" if len(os.Args) > 1 { name = os.Args[1] } app.run(name) }