224 lines
4.2 KiB
Go
224 lines
4.2 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|