diff --git a/README.md b/README.md
index e88d613..3ec2fd5 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@ Why? No need to manually edit an address book, yet the cached ranking is
available extremely fast.
### Features:
+
- scans all your emails
- ranks based on both recency and frequency of addresses
- collects from To, Cc, Bcc and From fields
@@ -16,13 +17,15 @@ available extremely fast.
- uses the most frequent non-empty display name for each email
- filters common "no reply" addresses, additional filters can be added via regexes
- normalizes emails to lower case
-- "blazingly fast"*: crunch time for 270k emails is 7s on my machine, grepping from the output is instantaneous
+- ability to add additional email addresses from a command
+- "blazingly fast"\*: crunch time for 270k emails is 7s on my machine, grepping from the output is instantaneous
-*: compared to original python implementation for crunching (see Behind the scenes below) and compared to using notmuch query for address completion
+\*: compared to original python implementation for crunching (see Behind the scenes below) and compared to using notmuch query for address completion
# Installation
The easiest way to install is running:
+
```
go install github.com/ferdinandyb/maildir-rank-addr@latest
```
@@ -41,6 +44,8 @@ or systemd timer).
Supported flags:
```
+ --addr-book-cmd string optional command to query addresses from your addressbook
+ --addr-book-add-unmatched if cmd is stated, determine wether to add unmatched addresses at the end of the file (true or false)
--addresses strings comma separated list of your email addresses (regex possible)
--config string path to config file
--filters strings comma separated list of regexes to filter
@@ -67,6 +72,7 @@ classification based on your explicit sends will not be possible!
Uses go's `text/template` to configure output for each address (one line per address).
Available keys:
+
```
Address
Name
@@ -101,6 +107,18 @@ before the @) matches any of these strings:
"nincsvalasz",
```
+**addr-book-cmd**
+
+Optional command to fetch email addresses and names, the output it returns must have
+an email address first, followed by a tab space and and the desired name, the name
+must end in a tab space or a newline for the command to work, this can be
+useful for integrating with command line addressbooks such as abook or khard
+
+```
+abook --mutt-query "s"
+khard email -p --remove-first-line
+```
+
**config**
Path to a config file to be loaded instead of the defaults (see below).
@@ -124,12 +142,12 @@ outputpath = "~/.mail/addressbook"
template = "{{.Address}}\t{{.Name}}"
```
-
## Integration
#### aerc
Put something like this in your aerc config (using your favourite grep):
+
```
address-book-cmd="ugrep -jP -m 100 --color=never %s /home/[myuser]/.cache/maildir-rank-addr/addressbook.tsv"
```
@@ -145,7 +163,6 @@ code the path without shell expansion.
Ranking is actually done by first classifying and then ranking within class.
-
### Classifying addresses
First we go through each email found in your maildir and for each address found
@@ -212,7 +229,6 @@ Please see [contribution guidelines](https://github.com/ferdinandyb/maildir-rank
- [notmuch-addrlookup-c](https://github.com/aperezdc/notmuch-addrlookup-c): address lookup from notmuch
- [addr-book-combine](https://jasoncarloscox.com/creations/addr-book-combine/): for combining generated addressbooks with hand currated ones, like [khard](https://github.com/lucc/khard)
-
# Acknowledgments
Some functions for parsing email was taken from [aerc](aerc-mail.org).
diff --git a/config.go b/config.go
index 9d5691b..08911ca 100644
--- a/config.go
+++ b/config.go
@@ -2,13 +2,15 @@ package main
import (
"fmt"
- homedir "github.com/mitchellh/go-homedir"
- "github.com/spf13/pflag"
- "github.com/spf13/viper"
"os"
+ "os/exec"
"regexp"
"strings"
"text/template"
+
+ homedir "github.com/mitchellh/go-homedir"
+ "github.com/spf13/pflag"
+ "github.com/spf13/viper"
)
func loadConfig() Config {
@@ -16,6 +18,8 @@ func loadConfig() Config {
pflag.String("maildir", "", "path to maildir folder")
pflag.String("outputpath", "", "path to output file")
pflag.String("template", "", "output template")
+ pflag.String("addr-book-cmd", "", "optional command to query addresses from your addressbook")
+ pflag.Bool("addr-book-add-unmatched", false, "flag to determine if you want unmatched addressbook contacts to be added to the output")
pflag.StringSlice("addresses", []string{}, "comma separated list of your email addresses (regex possible)")
pflag.StringSlice("filters", []string{}, "comma separated list of regexes to filter")
pflag.Parse()
@@ -65,6 +69,14 @@ func loadConfig() Config {
addresses[i] = regexp.MustCompile(filter)
}
templateString := viper.GetString("template")
+ addressbookLookupCommandString := viper.GetString("addr-book-cmd")
+ addressbookAddUnmatched := viper.GetBool("addr-book-add-unmatched")
+ var addressbookLookupCommand *exec.Cmd
+ if addressbookLookupCommandString != "" {
+ args := strings.Fields(addressbookLookupCommandString)
+ application, arguments := args[0], args[1:]
+ addressbookLookupCommand = exec.Command(application, arguments...)
+ }
if !strings.HasSuffix(templateString, "\n") {
templateString += "\n"
@@ -74,11 +86,13 @@ func loadConfig() Config {
panic(fmt.Errorf("bad template"))
}
config := Config{
- maildir: maildir,
- outputpath: outputpath,
- addresses: addresses,
- template: tmpl,
- customFilters: customFilters,
+ maildir: maildir,
+ outputpath: outputpath,
+ addresses: addresses,
+ template: tmpl,
+ customFilters: customFilters,
+ addressbookLookupCommand: addressbookLookupCommand,
+ addressbookAddUnmatched: addressbookAddUnmatched,
}
return config
}
diff --git a/data.go b/data.go
index 80d49ec..3e01c3a 100644
--- a/data.go
+++ b/data.go
@@ -1,6 +1,7 @@
package main
import (
+ "os/exec"
"regexp"
"text/template"
)
@@ -18,9 +19,11 @@ type AddressData struct {
}
type Config struct {
- maildir string
- outputpath string
- addresses []*regexp.Regexp
- template *template.Template
- customFilters []*regexp.Regexp
+ maildir string
+ outputpath string
+ addresses []*regexp.Regexp
+ template *template.Template
+ customFilters []*regexp.Regexp
+ addressbookLookupCommand *exec.Cmd
+ addressbookAddUnmatched bool
}
diff --git a/maildir-rank-addr.go b/maildir-rank-addr.go
index bcc1958..7a76887 100644
--- a/maildir-rank-addr.go
+++ b/maildir-rank-addr.go
@@ -1,10 +1,9 @@
package main
-import ()
-
func main() {
config := loadConfig()
- data := walkMaildir(config.maildir, config.addresses, config.customFilters)
+ addressbook := parseAddressbook(config.addressbookLookupCommand)
+ data := walkMaildir(config.maildir, config.addresses, config.customFilters, addressbook)
classeddata := calculateRanks(data)
- saveData(classeddata, config.outputpath, config.template)
+ saveData(classeddata, config.outputpath, config.template, addressbook, config.addressbookAddUnmatched)
}
diff --git a/output.go b/output.go
index 297f2b3..c5e037e 100644
--- a/output.go
+++ b/output.go
@@ -13,10 +13,11 @@ func saveData(
classedData map[int]map[string]AddressData,
path string,
tmpl *template.Template,
+ addressbook map[string]string,
+ addUnmatched bool,
) {
os.MkdirAll(filepath.Dir(path), os.ModePerm)
f, err := os.Create(path)
-
if err != nil {
log.Fatal(err)
}
@@ -46,5 +47,14 @@ func saveData(
tmpl.Execute(f, kv.Value)
}
}
+ if addUnmatched {
+ for ak, av := range addressbook {
+ aD := AddressData{}
+ aD.Address = ak
+ aD.Name = av
+ count++
+ tmpl.Execute(f, aD)
+ }
+ }
fmt.Println(count, " addresses written to ", path)
}
diff --git a/parseAddressbook.go b/parseAddressbook.go
new file mode 100644
index 0000000..68c1235
--- /dev/null
+++ b/parseAddressbook.go
@@ -0,0 +1,38 @@
+package main
+
+import (
+ "bufio"
+ "fmt"
+ "log"
+ "os/exec"
+ "strings"
+)
+
+func parseAddressbook(
+ cmd *exec.Cmd,
+) map[string]string {
+ if cmd == nil {
+ return nil
+ }
+ addressbook := make(map[string]string)
+ out, err := cmd.StdoutPipe()
+ if err != nil {
+ log.Fatal(err)
+ }
+ if err := cmd.Start(); err != nil {
+ log.Fatal(err)
+ }
+ scanner := bufio.NewScanner(out)
+ for scanner.Scan() {
+ slice := strings.Split(scanner.Text(), "\t")
+ if len(slice) < 2 {
+ fmt.Println("Couldn't parse ", scanner.Text())
+ } else {
+ addressbook[strings.ToLower(slice[0])] = slice[1]
+ }
+ }
+ if err := cmd.Wait(); err != nil {
+ log.Fatal(err)
+ }
+ return addressbook
+}
diff --git a/parseMail.go b/parseMail.go
index b0d739e..00af094 100644
--- a/parseMail.go
+++ b/parseMail.go
@@ -95,6 +95,7 @@ func processHeaders(
retvalchan chan map[string]AddressData,
addresses []*regexp.Regexp,
customFilters []*regexp.Regexp,
+ addressbook map[string]string,
) {
count := 0
retval := make(map[string]AddressData)
@@ -124,12 +125,6 @@ func processHeaders(
continue
}
for _, address := range header {
-
- dec := new(mime.WordDecoder)
- name, err := dec.DecodeHeader(address.Name)
- if err != nil {
- continue
- }
normaddr := strings.ToLower(address.Address)
if filterAddress(normaddr, customFilters) {
continue
@@ -140,8 +135,15 @@ func processHeaders(
addresses,
)
if aD, ok := retval[normaddr]; ok {
- if normaddr != strings.ToLower(name) {
- aD.Names = append(aD.Names, name)
+ if aD.Name == "" {
+ dec := new(mime.WordDecoder)
+ name, err := dec.DecodeHeader(address.Name)
+ if err != nil {
+ continue
+ }
+ if (strings.ToLower(name) != normaddr) && (strings.ToLower(name) != "") {
+ aD.Names = append(aD.Names, name)
+ }
}
if aD.Class < class {
aD.Class = class
@@ -153,11 +155,22 @@ func processHeaders(
retval[normaddr] = aD
} else {
aD := AddressData{}
+ addressbookname := addressbook[normaddr]
+ if addressbookname == "" {
+ dec := new(mime.WordDecoder)
+ name, err := dec.DecodeHeader(address.Name)
+ if err != nil {
+ continue
+ }
+ if (strings.ToLower(name) != normaddr) && (strings.ToLower(name) != "") {
+ aD.Names = append(aD.Names, name)
+ }
+ } else {
+ aD.Name = addressbookname
+ delete(addressbook, normaddr)
+ }
aD.Address = normaddr
aD.Class = class
- if normaddr != strings.ToLower(name) {
- aD.Names = append(aD.Names, name)
- }
aD.ClassDate = [3]int64{0, 0, 0}
aD.ClassDate[class] = time.Unix()
aD.ClassCount = [3]int{0, 0, 0}
@@ -174,7 +187,7 @@ func processHeaders(
close(retvalchan)
}
-func walkMaildir(path string, addresses []*regexp.Regexp, customFilters []*regexp.Regexp) map[string]AddressData {
+func walkMaildir(path string, addresses []*regexp.Regexp, customFilters []*regexp.Regexp, addressbook map[string]string) map[string]AddressData {
headers := make(chan *mail.Header)
messagePaths := make(chan string, 4096)
@@ -189,7 +202,7 @@ func walkMaildir(path string, addresses []*regexp.Regexp, customFilters []*regex
}
retvalchan := make(chan map[string]AddressData)
- go processHeaders(headers, retvalchan, addresses, customFilters)
+ go processHeaders(headers, retvalchan, addresses, customFilters, addressbook)
filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err != nil {
diff --git a/ranking.go b/ranking.go
index 7f19d27..b8b7022 100644
--- a/ranking.go
+++ b/ranking.go
@@ -32,6 +32,9 @@ func getMostFrequent(names []string) string {
}
func normalizeAddressNames(aD AddressData) AddressData {
+ if aD.Name != "" {
+ return aD
+ }
aD.Name = getMostFrequent(aD.Names)
return aD
}