package main import ( "encoding/json" "flag" "fmt" "os" "path/filepath" "strings" "xinu.tv/email" "xinu.tv/email/maildir" "github.com/golang/glog" ) var ( dryrun = flag.Bool("dryrun", true, "don't actually rename files, just log actions.") saveFile = flag.String("save", "", "filename to store unread status.") loadFile = flag.String("load", "", "filename to load unread status.") maildirPath = flag.String("maildir", "", "Maildir root") skipFiles = flag.String("skip", "maildirfolder,log,msgid.cache,razor-agent.log", "comma separated files to skip") ) type Status struct { Hash string `json:"hash"` Path string `json:"path,omitempthashy"` Read bool `json:"read,omitempty"` } type Messages struct { // Filenames to skip when hash mail. Skip map[string]struct{} // Read mail status. Statuses []Status } func (m *Messages) hashMail(path string, info os.FileInfo, err error) error { glog.Infoln("Processing", path) if err != nil { return err } if info.IsDir() { return nil } base := filepath.Base(path) if _, ok := m.Skip[base]; ok { glog.Infoln("Skipping", path) return nil } r, err := os.Open(path) if err != nil { glog.Fatal(err) } defer r.Close() h, err := email.HashReader(r) if err != nil { glog.Errorf("%s not an mail file", path) glog.Infof("%q", err.Error()) return nil } md := maildir.NewInfo(path) m.Statuses = append(m.Statuses, Status{ Path: path, Hash: h, Read: md.Seen, }) return nil } func (m *Messages) LoadStatus(statusFile string) error { glog.Infof("Loading file %q", statusFile) r, err := os.Open(statusFile) if err != nil { return nil } defer r.Close() dec := json.NewDecoder(r) return dec.Decode(&m.Statuses) } func (m *Messages) SaveStatus(maildirPath, statusFile string) error { if err := filepath.Walk(maildirPath, m.hashMail); err != nil { return err } b, err := json.MarshalIndent(m, "", " ") if err != nil { return err } glog.Infof("Saving file %q", statusFile) f, err := os.Create(statusFile) if err != nil { return err } defer f.Close() n, err := f.Write(b) if err != nil { return err } if n != len(b) { return fmt.Errorf("Short write of save status: wrote %d of %d", n, len(b)) } return nil } func (m Messages) Reconcile(maildirPath string) error { hashMap := make(map[string]*Status, len(m.Statuses)) for i, msg := range m.Statuses { hashMap[msg.Hash] = &m.Statuses[i] } return filepath.Walk(maildirPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } base := filepath.Base(path) if _, ok := m.Skip[base]; ok { glog.Infoln("Skipping", path) return nil } r, err := os.Open(path) if err != nil { glog.Fatal(err) } defer r.Close() chksum, err := email.HashReader(r) if err != nil { glog.Errorf("%s not an mail file", path) glog.Infof("%q", err.Error()) return nil } md := maildir.NewInfo(path) s, ok := hashMap[chksum] if !ok { return nil } glog.V(2).Infof("Comparing flags of %q to %q", path, s.Path) if md.Seen != s.Read { md.Seen = s.Read newPath := md.String() glog.Infof("%s => %s", path, newPath) if !*dryrun { os.Rename(path, newPath) } } return nil }) } func (m Messages) PrintStats() { r, u, t := 0, 0, 0 for _, msg := range m.Statuses { t++ if msg.Read { r++ } else { u++ } } fmt.Println(r, "read") fmt.Println(u, "unread") fmt.Println(t, "total") } func main() { defer glog.Flush() flag.Parse() skipStr := strings.Split(*skipFiles, ",") skip := make(map[string]struct{}, len(skipStr)) for _, s := range skipStr { skip[s] = struct{}{} } glog.Infoln("Skip files", skip) m := Messages{ Skip: skip, } if *maildirPath == "" { fmt.Println("Must specify Maildir with -maildir") os.Exit(1) } if *saveFile == "" && *loadFile == "" { fmt.Println("Must specify one of -save or -load") os.Exit(1) } if *saveFile != "" { if err := m.SaveStatus(*maildirPath, *saveFile); err != nil { glog.Fatal(err) } m.PrintStats() } if *loadFile != "" { if err := m.LoadStatus(*loadFile); err != nil { glog.Fatal(err) } m.PrintStats() if err := m.Reconcile(*maildirPath); err != nil { glog.Fatal(err) } } }