1package main 2 3// stdlib imports 4import "os" 5import "path" 6import "log" 7import "fmt" 8import "regexp" 9import "strings" 10import "sort" 11 12// 3rd-party imports 13import "notmuch" 14import "github.com/msbranco/goconfig" 15 16type mail_addr_freq struct { 17 addr string 18 count [3]uint 19} 20 21type frequencies map[string]uint 22 23/* Used to sort the email addresses from most to least used */ 24func sort_by_freq(m1, m2 *mail_addr_freq) int { 25 if m1.count[0] == m2.count[0] && 26 m1.count[1] == m2.count[1] && 27 m1.count[2] == m2.count[2] { 28 return 0 29 } 30 31 if m1.count[0] > m2.count[0] || 32 m1.count[0] == m2.count[0] && 33 m1.count[1] > m2.count[1] || 34 m1.count[0] == m2.count[0] && 35 m1.count[1] == m2.count[1] && 36 m1.count[2] > m2.count[2] { 37 return -1 38 } 39 40 return 1 41} 42 43type maddresses []*mail_addr_freq 44 45func (self *maddresses) Len() int { 46 return len(*self) 47} 48 49func (self *maddresses) Less(i, j int) bool { 50 m1 := (*self)[i] 51 m2 := (*self)[j] 52 v := sort_by_freq(m1, m2) 53 if v <= 0 { 54 return true 55 } 56 return false 57} 58 59func (self *maddresses) Swap(i, j int) { 60 (*self)[i], (*self)[j] = (*self)[j], (*self)[i] 61} 62 63// find most frequent real name for each mail address 64func frequent_fullname(freqs frequencies) string { 65 var maxfreq uint = 0 66 fullname := "" 67 freqs_sz := len(freqs) 68 69 for mail, freq := range freqs { 70 if (freq > maxfreq && mail != "") || freqs_sz == 1 { 71 // only use the entry if it has a real name 72 // or if this is the only entry 73 maxfreq = freq 74 fullname = mail 75 } 76 } 77 return fullname 78} 79 80func addresses_by_frequency(msgs *notmuch.Messages, name string, pass uint, addr_to_realname *map[string]*frequencies) *frequencies { 81 82 freqs := make(frequencies) 83 84 pattern := `\s*(("(\.|[^"])*"|[^,])*<?(?mail\b\w+([-+.]\w+)*\@\w+[-\.\w]*\.([-\.\w]+)*\w\b)>?)` 85 // pattern := "\\s*((\\\"(\\\\.|[^\\\\\"])*\\\"|[^,])*" + 86 // "<?(?P<mail>\\b\\w+([-+.]\\w+)*\\@\\w+[-\\.\\w]*\\.([-\\.\\w]+)*\\w\\b)>?)" 87 pattern = `.*` + strings.ToLower(name) + `.*` 88 var re *regexp.Regexp = nil 89 var err error = nil 90 if re, err = regexp.Compile(pattern); err != nil { 91 log.Printf("error: %v\n", err) 92 return &freqs 93 } 94 95 headers := []string{"from"} 96 if pass == 1 { 97 headers = append(headers, "to", "cc", "bcc") 98 } 99 100 for ; msgs.Valid(); msgs.MoveToNext() { 101 msg := msgs.Get() 102 //println("==> msg [", msg.GetMessageId(), "]") 103 for _, header := range headers { 104 froms := strings.ToLower(msg.GetHeader(header)) 105 //println(" froms: ["+froms+"]") 106 for _, from := range strings.Split(froms, ",") { 107 from = strings.Trim(from, " ") 108 match := re.FindString(from) 109 //println(" -> match: ["+match+"]") 110 occ, ok := freqs[match] 111 if !ok { 112 freqs[match] = 0 113 occ = 0 114 } 115 freqs[match] = occ + 1 116 } 117 } 118 } 119 return &freqs 120} 121 122func search_address_passes(queries [3]*notmuch.Query, name string) []string { 123 var val []string 124 addr_freq := make(map[string]*mail_addr_freq) 125 addr_to_realname := make(map[string]*frequencies) 126 127 var pass uint = 0 // 0-based 128 for _, query := range queries { 129 if query == nil { 130 //println("**warning: idx [",idx,"] contains a nil query") 131 continue 132 } 133 msgs := query.SearchMessages() 134 ht := addresses_by_frequency(msgs, name, pass, &addr_to_realname) 135 for addr, count := range *ht { 136 freq, ok := addr_freq[addr] 137 if !ok { 138 freq = &mail_addr_freq{addr: addr, count: [3]uint{0, 0, 0}} 139 } 140 freq.count[pass] = count 141 addr_freq[addr] = freq 142 } 143 msgs.Destroy() 144 pass += 1 145 } 146 147 addrs := make(maddresses, len(addr_freq)) 148 { 149 iaddr := 0 150 for _, freq := range addr_freq { 151 addrs[iaddr] = freq 152 iaddr += 1 153 } 154 } 155 sort.Sort(&addrs) 156 157 for _, addr := range addrs { 158 freqs, ok := addr_to_realname[addr.addr] 159 if ok { 160 val = append(val, frequent_fullname(*freqs)) 161 } else { 162 val = append(val, addr.addr) 163 } 164 } 165 //println("val:",val) 166 return val 167} 168 169type address_matcher struct { 170 // the notmuch database 171 db *notmuch.Database 172 // full path of the notmuch database 173 user_db_path string 174 // user primary email 175 user_primary_email string 176 // user tag to mark from addresses as in the address book 177 user_addrbook_tag string 178} 179 180func new_address_matcher() *address_matcher { 181 // honor NOTMUCH_CONFIG 182 home := os.Getenv("NOTMUCH_CONFIG") 183 if home == "" { 184 home = os.Getenv("HOME") 185 } 186 187 cfg, err := goconfig.ReadConfigFile(path.Join(home, ".notmuch-config")) 188 if err != nil { 189 log.Fatalf("error loading config file:", err) 190 } 191 192 db_path, _ := cfg.GetString("database", "path") 193 primary_email, _ := cfg.GetString("user", "primary_email") 194 addrbook_tag, err := cfg.GetString("user", "addrbook_tag") 195 if err != nil { 196 addrbook_tag = "addressbook" 197 } 198 199 self := &address_matcher{db: nil, 200 user_db_path: db_path, 201 user_primary_email: primary_email, 202 user_addrbook_tag: addrbook_tag} 203 return self 204} 205 206func (self *address_matcher) run(name string) { 207 queries := [3]*notmuch.Query{} 208 209 // open the database 210 if db, status := notmuch.OpenDatabase(self.user_db_path, 211 notmuch.DATABASE_MODE_READ_ONLY); status == notmuch.STATUS_SUCCESS { 212 self.db = db 213 } else { 214 log.Fatalf("Failed to open the database: %v\n", status) 215 } 216 217 // pass 1: look at all from: addresses with the address book tag 218 query := "tag:" + self.user_addrbook_tag 219 if name != "" { 220 query = query + " and from:" + name + "*" 221 } 222 queries[0] = self.db.CreateQuery(query) 223 224 // pass 2: look at all to: addresses sent from our primary mail 225 query = "" 226 if name != "" { 227 query = "to:" + name + "*" 228 } 229 if self.user_primary_email != "" { 230 query = query + " from:" + self.user_primary_email 231 } 232 queries[1] = self.db.CreateQuery(query) 233 234 // if that leads only to a few hits, we check every from too 235 if queries[0].CountMessages()+queries[1].CountMessages() < 10 { 236 query = "" 237 if name != "" { 238 query = "from:" + name + "*" 239 } 240 queries[2] = self.db.CreateQuery(query) 241 } 242 243 // actually retrieve and sort addresses 244 results := search_address_passes(queries, name) 245 for _, v := range results { 246 if v != "" && v != "\n" { 247 fmt.Println(v) 248 } 249 } 250 return 251} 252 253func main() { 254 //fmt.Println("args:",os.Args) 255 app := new_address_matcher() 256 name := "" 257 if len(os.Args) > 1 { 258 name = os.Args[1] 259 } 260 app.run(name) 261} 262