Skip to content

Commit

Permalink
Ability to use external contact management software for assigning names
Browse files Browse the repository at this point in the history
Currently names for addresses are assigned based on how common they are
in the users email, but this might not be always preferable. Add the
ability to override names calculated by maildir-rank-addr using an
external address lookup command.
  • Loading branch information
boscovn authored and ferdinandyb committed Jan 30, 2023
1 parent 6bd8741 commit 022d14a
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 36 deletions.
26 changes: 21 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"<sup>*</sup>: 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"<sup>\*</sup>: crunch time for 270k emails is 7s on my machine, grepping from the output is instantaneous

<sup>*</sup>: compared to original python implementation for crunching (see Behind the scenes below) and compared to using notmuch query for address completion
<sup>\*</sup>: 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
```
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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).
Expand All @@ -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"
```
Expand All @@ -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
Expand Down Expand Up @@ -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).
30 changes: 22 additions & 8 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,24 @@ 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 {
pflag.String("config", "", "path to config file")
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()
Expand Down Expand Up @@ -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"
Expand All @@ -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
}
13 changes: 8 additions & 5 deletions data.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"os/exec"
"regexp"
"text/template"
)
Expand All @@ -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
}
7 changes: 3 additions & 4 deletions maildir-rank-addr.go
Original file line number Diff line number Diff line change
@@ -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)
}
12 changes: 11 additions & 1 deletion output.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
38 changes: 38 additions & 0 deletions parseAddressbook.go
Original file line number Diff line number Diff line change
@@ -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
}
39 changes: 26 additions & 13 deletions parseMail.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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}
Expand All @@ -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)

Expand All @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions ranking.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit 022d14a

Please sign in to comment.