diff --git a/watch/watcher.go b/watch/watcher.go new file mode 100644 index 0000000..e66507c --- /dev/null +++ b/watch/watcher.go @@ -0,0 +1,188 @@ +package main + +import ( + "flag" + "log" + "os" + "path/filepath" + "sort" + "strings" + "time" +) + +var ( + root = flag.String("root", filepath.Join(os.Getenv("HOME"), "Maildir"), + "root directory to watch") + poll = flag.Duration("poll", time.Second*time.Duration(10), + "poll interval between new file check") +) + +func folderFromPath(path string) string { + const sep = string(filepath.Separator) + parts := strings.Split(path, sep) + var folder string + if isMailDir(path) { + folder = parts[len(parts)-2] + } else { + folder = parts[len(parts)-1] + } + if folder[0] == '.' { + return strings.Replace(folder[1:], ".", "/", -1) + } + return "Inbox" +} + +// isMailDirParent determines if path is a Maildir folder's +// cur/new/tmp parent directory. +func isMailDirParent(path string) bool { + if isMailDir(path) { + return false + } + fi, err := os.Stat(filepath.Join(path, "new")) + if err != nil { + return false + } + return fi.IsDir() +} + +// isMailDir determins if path is one of Maildir's cur/new/tmp mail folders. +func isMailDir(path string) bool { + return strings.HasSuffix(path, "new") || strings.HasSuffix(path, "cur") || strings.HasSuffix(path, "tmp") +} + +// isNewEmail parses path looking for Maildir filename patterns for the flags +// indicating if a message has been seen. +func isNewEmail(path string) bool { + // More docs on Maildir flags at http://cr.yp.to/proto/maildir.html + // + // Seen: + // 1340520791.M843511P28536.sagan.sf.xinu.tv,S=1467,W=1503:2,RS + // Unseen: + // 1377995543.50996_2.sagan.sf.xinu.tv:2,Rb + seen := false + i := strings.LastIndex(path, ":") + if i != -1 { + info := path[i:] + i = strings.Index(info, ",") + if i == -1 { + return true + } + + flags := info[i:] + seen = strings.Contains(flags, "S") + } + return !seen +} + +type mailCounter struct { + lastrun time.Time + counts map[string]int +} + +func (mc *mailCounter) walk(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + log.Println("path:", path) + if info.ModTime().Before(mc.lastrun) { + // Don't descend subdirectories older than our last run. + return filepath.SkipDir + } + if isMailDirParent(path) { + // This is a parent directory, or not a proper mail dir. Set the + // counts to zero in either case. + folder := folderFromPath(path) + log.Println("Setting folder to 0", folder) + mc.counts[folder] = 0 + } + return nil + } + dn := filepath.Dir(path) + if isMailDir(dn) { + if isNewEmail(path) { + folder := folderFromPath(dn) + mc.counts[folder]++ + } + } + return nil +} + +type countList []count + +type count struct { + f string + c int +} + +func (cl countList) Len() int { return len(cl) } +func (cl countList) Less(i, j int) bool { + if cl[i].c == cl[j].c { + return cl[i].f < cl[j].f + } + return cl[i].c > cl[j].c +} +func (cl countList) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] } + +func countListFromMap(counts map[string]int) countList { + cl := make([]count, len(counts)) + i := 0 + for k, v := range counts { + cl[i].f = k + cl[i].c = v + i++ + } + return cl +} + +// printCounts prints the folders with unread messages in descending order, +// sorting alphabetically for tie-breakers +func printCounts(counts map[string]int) { + cl := countListFromMap(counts) + sort.Sort(cl) + for _, c := range cl { + if c.c > 0 { + log.Printf("%s (%d)", c.f, c.c) + } + } +} + +func (mc *mailCounter) watch(root string, poll time.Duration) error { + for { + fi, err := os.Stat(root) + if err != nil { + return err + } + if fi.ModTime().After(mc.lastrun) { + thisrun := time.Now() + log.Println("Mod detected", fi.ModTime(), ">", mc.lastrun) + err = filepath.Walk(root, mc.walk) + if err != nil { + return err + } + printCounts(mc.counts) + + mc.lastrun = thisrun + } else { + log.Println("No change detected") + } + time.Sleep(poll) + } + return nil +} + +func newMailCounter() *mailCounter { + return &mailCounter{ + counts: map[string]int{}, + } +} + +func main() { + flag.Parse() + + mc := newMailCounter() + err := mc.watch(*root, *poll) + if err != nil { + log.Fatal(err) + } +} diff --git a/watch/watcher_test.go b/watch/watcher_test.go new file mode 100644 index 0000000..ce3f128 --- /dev/null +++ b/watch/watcher_test.go @@ -0,0 +1,29 @@ +package main + +import ( + "testing" +) + +func TestIsNewEmail(t *testing.T) { + data := []struct { + isnew bool + path string + }{ + { + isnew: true, + path: "1377995543.50996_2.sagan.sf.xinu.tv:2,Rb", + }, + { + isnew: false, + path: "1340520791.M843511P28536.sagan.sf.xinu.tv,S=1467,W=1503:2,RS", + }, + } + + for _, d := range data { + got := isNewEmail(d.path) + want := d.isnew + if got != want { + t.Error("Got", got, "want", want, "for", d.path) + } + } +}