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) } }