1package main
2
3import (
4 "flag"
5 "fmt"
6 "log"
7 "os"
8 "path/filepath"
9 "strings"
10 "sync"
11)
12
13var (
14 verbose = flag.Bool("v", false, "print out performed changes")
15 dryrun = flag.Bool("d", false, "don't perform any changes, combine with -v")
16)
17
18type MailWalkFn func(mail *Mail, db *MailDatabase, err error) error
19
20func indexOldMsgs(mail *Mail, db *MailDatabase, err error) error {
21 if err != nil {
22 panic(err)
23 }
24
25 db.AddOldMessage(mail)
26 return nil
27}
28
29func indexNewMsgs(mail *Mail, db *MailDatabase, err error) error {
30 if err != nil {
31 panic(err)
32 }
33
34 oldMail, err := db.GetOldMessage(mail)
35 if err != nil {
36 return err
37 }
38
39 if oldMail == nil {
40 db.AddNewMessage(nil, mail)
41 } else if !oldMail.IsSame(mail) {
42 db.AddNewMessage(oldMail, mail)
43 }
44
45 return nil
46}
47
48func walkMaildir(maildir string, db *MailDatabase, walkFn MailWalkFn) error {
49 wrapFn := func(path string, info os.FileInfo, err error) error {
50 handleError := func(err error) error { return walkFn(nil, nil, err) }
51 if err != nil {
52 return handleError(err)
53 }
54
55 if info.IsDir() {
56 if !isMaildirFn(info.Name()) {
57 return handleError(fmt.Errorf("unexpected directory %q", info.Name()))
58 } else {
59 return nil
60 }
61 }
62
63 mail, err := NewMail(maildir, path)
64 if err != nil {
65 return handleError(err)
66 }
67
68 return walkFn(mail, db, err)
69 }
70
71 for _, dir := range []string{"cur", "new"} {
72 err := filepath.Walk(filepath.Join(maildir, dir), wrapFn)
73 if err != nil {
74 return err
75 }
76 }
77
78 return nil
79}
80
81// Returns mapping new maildir → old maildir.
82func parseArgs(args []string) (map[string]string, error) {
83 parsedArgs := make(map[string]string)
84 for _, arg := range args {
85 splitted := strings.Split(arg, "→")
86 if len(splitted) != 2 {
87 return nil, fmt.Errorf("invalid argument %q", arg)
88 }
89
90 new := splitted[0]
91 old := splitted[1]
92 for _, dir := range []string{old, new} {
93 if !isValidMaildir(dir) {
94 return nil, fmt.Errorf("%q is not a valid maildir", dir)
95 }
96 }
97
98 if _, ok := parsedArgs[new]; ok {
99 return nil, fmt.Errorf("duplicate maildir %q", arg)
100 }
101 parsedArgs[new] = old
102 }
103 return parsedArgs, nil
104}
105
106func indexMsgs(args map[string]string) (*MailDatabase, error) {
107 var wg sync.WaitGroup
108 db := NewMailDatabase()
109
110 wfn := func(dir string, mfn MailWalkFn) {
111 defer wg.Done()
112 err := walkMaildir(dir, db, mfn)
113 if err != nil {
114 log.Fatal(err)
115 }
116 }
117
118 wg.Add(len(args))
119 for _, old := range args {
120 go wfn(old, indexOldMsgs)
121 }
122 wg.Wait()
123
124 wg.Add(len(args))
125 for new, _ := range args {
126 go wfn(new, indexNewMsgs)
127 }
128 wg.Wait()
129
130 return db, nil
131}
132
133func archiveMsgs(args map[string]string, db *MailDatabase) error {
134 for _, new := range db.newMsgs {
135 if *verbose {
136 fmt.Printf("new: %s\n", new)
137 }
138 if *dryrun {
139 continue
140 }
141
142 err := new.CopyTo(args[new.maildir])
143 if err != nil {
144 return err
145 }
146 }
147 for _, pair := range db.modMsgs {
148 if *verbose {
149 fmt.Printf("move: %s → %s\n", pair.old, pair.new)
150 }
151 if *dryrun {
152 continue
153 }
154
155 destDir := args[pair.new.maildir]
156 newFp := filepath.Join(destDir, pair.new.directory, pair.new.name)
157 err := os.Rename(pair.old.Path(), newFp)
158 if err != nil {
159 return err
160 }
161 }
162
163 return nil
164}
165
166func main() {
167 log.SetFlags(log.Lshortfile)
168
169 flag.Usage = func() {
170 fmt.Fprintf(flag.CommandLine.Output(),
171 "Usage: %s [-v] [-d] MAILDIR_CURRENT→MAILDIR_ARCHIVE...\n\n", os.Args[0])
172 flag.PrintDefaults()
173 }
174 flag.Parse()
175
176 if flag.NArg() < 1 {
177 flag.Usage()
178 os.Exit(1)
179 }
180 args, err := parseArgs(flag.Args())
181 if err != nil {
182 log.Fatal(err)
183 }
184
185 db, err := indexMsgs(args)
186 if err != nil {
187 log.Fatal(err)
188 }
189 err = archiveMsgs(args, db)
190 if err != nil {
191 log.Fatal(err)
192 }
193}