diff --git a/cmd/mailhash/mailhash.go b/cmd/mailhash/mailhash.go new file mode 100644 index 0000000..e94106c --- /dev/null +++ b/cmd/mailhash/mailhash.go @@ -0,0 +1,94 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + "path/filepath" + + "xinu.tv/email" + + "github.com/golang/glog" +) + +var ( + saveFile = flag.String("save", "", "filename to store unread status.") + loadFile = flag.String("load", "", "filename to load unread status.") + maildir = flag.String("maildir", "", "Maildir root") +) + +type Status struct { + Hash string `json:"hash"` + Path string `json:"path,omitempthashy"` + Unread bool `json:"unread,omitempty"` +} + +type Messages []Status + +func (m *Messages) hashMail(path string, info os.FileInfo, err error) error { + if info.IsDir() { + return nil + } + r, err := os.Open(path) + if err != nil { + glog.Fatal(err) + } + headers := []string{"to", "from", "cc", "date", "subject"} + h, err := email.Hash(r, headers) + if err != nil { + glog.Errorln(path, err) + return nil + } + md := email.NewInfo(path) + //fmt.Printf("%x %s\n", h.Sum(nil), path) + *m = append(*m, Status{ + Path: path, + Hash: fmt.Sprintf("%x", h.Sum(nil)), + Unread: !md.Seen, + }) + return nil +} + +func (m *Messages) SaveStatus(maildir, saveFile string) error { + if err := filepath.Walk(maildir, m.hashMail); err != nil { + return err + } + + b, err := json.MarshalIndent(m, "", " ") + if err != nil { + return err + } + + f, err := os.Create(saveFile) + 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 main() { + defer glog.Flush() + flag.Parse() + m := Messages{} + + if *maildir == "" { + glog.Fatal("Must specify Maildir with -maildir") + } + + if *saveFile != "" { + if err := m.SaveStatus(*maildir, *saveFile); err != nil { + glog.Fatal(err) + } + } +} diff --git a/hash.go b/hash.go new file mode 100644 index 0000000..b91ed46 --- /dev/null +++ b/hash.go @@ -0,0 +1,38 @@ +package email + +import ( + "crypto/sha1" + "hash" + "io" + "net/mail" + "os" + "sort" + + "github.com/golang/glog" +) + +// Hash will parse r as an email, and return a hash.Hash that has been applied +// to the values of the specified headers. +func Hash(r io.Reader, headers []string) (hash.Hash, error) { + // Add deterministic behavior regardless of the order the users specified. + sort.Strings(headers) + var name string + if f, ok := r.(*os.File); ok { + name = f.Name() + } + + h := sha1.New() + msg, err := mail.ReadMessage(r) + if err != nil { + return nil, err + } + + for _, header := range headers { + v := msg.Header.Get(header) + if v == "" { + glog.V(2).Infoln(name, "Empty", header, "header") + } + io.WriteString(h, v) + } + return h, nil +}