notmuch/bindings/go/cmds/notmuch-addrlookup.go
2011-01-26 15:59:19 +10:00

260 lines
No EOL
6 KiB
Go

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/jteeuwen/go-pkg-ini/ini"
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*(("(\.|[^"])*"|[^,])*<?(?mail\b\w+([-+.]\w+)*\@\w+[-\.\w]*\.([-\.\w]+)*\w\b)>?)`
// pattern := "\\s*((\\\"(\\\\.|[^\\\\\"])*\\\"|[^,])*" +
// "<?(?P<mail>\\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.Exitf("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
self.db = notmuch.OpenDatabase(self.user_db_path,
notmuch.DATABASE_MODE_READ_ONLY)
// 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)
}