archive-mail

Maintains maildir archives synced with current maildirs

git clone https://git.8pit.net/archive-mail.git

  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}