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