14 import "github.com/kless/goconfig/config"
16 type mail_addr_freq struct {
21 type frequencies map[string]uint
23 /* Used to sort the email addresses from most to least used */
24 func 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] {
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] {
43 type maddresses []*mail_addr_freq
45 func (self *maddresses) Len() int {
49 func (self *maddresses) Less(i, j int) bool {
52 v := sort_by_freq(m1, m2)
59 func (self *maddresses) Swap(i, j int) {
60 (*self)[i], (*self)[j] = (*self)[j], (*self)[i]
63 // find most frequent real name for each mail address
64 func frequent_fullname(freqs frequencies) string {
67 freqs_sz := len(freqs)
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
80 func addresses_by_frequency(msgs *notmuch.Messages, name string, pass uint, addr_to_realname *map[string]*frequencies) *frequencies {
82 freqs := make(frequencies)
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
90 if re, err = regexp.Compile(pattern); err != nil {
91 log.Printf("error: %v\n", err)
95 headers := []string{"from"}
97 headers = append(headers, "to", "cc", "bcc")
100 for ; msgs.Valid(); msgs.MoveToNext() {
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]
115 freqs[match] = occ + 1
122 func search_address_passes(queries [3]*notmuch.Query, name string) []string {
124 addr_freq := make(map[string]*mail_addr_freq)
125 addr_to_realname := make(map[string]*frequencies)
127 var pass uint = 0 // 0-based
128 for _, query := range queries {
130 //println("**warning: idx [",idx,"] contains a nil query")
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]
138 freq = &mail_addr_freq{addr: addr, count: [3]uint{0, 0, 0}}
140 freq.count[pass] = count
141 addr_freq[addr] = freq
147 addrs := make(maddresses, len(addr_freq))
150 for _, freq := range addr_freq {
157 for _, addr := range addrs {
158 freqs, ok := addr_to_realname[addr.addr]
160 val = append(val, frequent_fullname(*freqs))
162 val = append(val, addr.addr)
165 //println("val:",val)
169 type address_matcher struct {
170 // the notmuch database
172 // full path of the notmuch database
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
180 func new_address_matcher() *address_matcher {
181 var cfg *config.Config
184 // honor NOTMUCH_CONFIG
185 home := os.Getenv("NOTMUCH_CONFIG")
187 home = os.Getenv("HOME")
190 if cfg, err = config.ReadDefault(path.Join(home, ".notmuch-config")); err != nil {
191 log.Fatalf("error loading config file:", err)
194 db_path, _ := cfg.String("database", "path")
195 primary_email, _ := cfg.String("user", "primary_email")
196 addrbook_tag, err := cfg.String("user", "addrbook_tag")
198 addrbook_tag = "addressbook"
201 self := &address_matcher{db: nil,
202 user_db_path: db_path,
203 user_primary_email: primary_email,
204 user_addrbook_tag: addrbook_tag}
208 func (self *address_matcher) run(name string) {
209 queries := [3]*notmuch.Query{}
212 if db, status := notmuch.OpenDatabase(self.user_db_path,
213 notmuch.DATABASE_MODE_READ_ONLY); status == notmuch.STATUS_SUCCESS {
216 log.Fatalf("Failed to open the database: %v\n", status)
219 // pass 1: look at all from: addresses with the address book tag
220 query := "tag:" + self.user_addrbook_tag
222 query = query + " and from:" + name + "*"
224 queries[0] = self.db.CreateQuery(query)
226 // pass 2: look at all to: addresses sent from our primary mail
229 query = "to:" + name + "*"
231 if self.user_primary_email != "" {
232 query = query + " from:" + self.user_primary_email
234 queries[1] = self.db.CreateQuery(query)
236 // if that leads only to a few hits, we check every from too
237 if queries[0].CountMessages()+queries[1].CountMessages() < 10 {
240 query = "from:" + name + "*"
242 queries[2] = self.db.CreateQuery(query)
245 // actually retrieve and sort addresses
246 results := search_address_passes(queries, name)
247 for _, v := range results {
248 if v != "" && v != "\n" {
256 //fmt.Println("args:",os.Args)
257 app := new_address_matcher()
259 if len(os.Args) > 1 {