1// The maildir package provides an interface to mailboxes in the Maildir format.
2//
3// Maildir mailboxes are designed to be safe for concurrent delivery. This
4// means that at the same time, multiple processes can deliver to the same
5// mailbox. However only one process can receive and read messages stored in
6// the Maildir.
7package maildir
8
9import (
10	"crypto/rand"
11	"encoding/hex"
12	"io"
13	"os"
14	"path/filepath"
15	"sort"
16	"strconv"
17	"strings"
18	"sync/atomic"
19	"time"
20)
21
22// The separator separates a messages unique key from its flags in the filename.
23// This should only be changed on operating systems where the colon isn't
24// allowed in filenames.
25var separator rune = ':'
26
27var id int64 = 10000
28
29// createMode holds the permissions used when creating a directory.
30const createMode = 0700
31
32// A KeyError occurs when a key matches more or less than one message.
33type KeyError struct {
34	Key string // the (invalid) key
35	N   int    // number of matches (!= 1)
36}
37
38func (e *KeyError) Error() string {
39	return "maildir: key " + e.Key + " matches " + strconv.Itoa(e.N) + " files."
40}
41
42// A FlagError occurs when a non-standard info section is encountered.
43type FlagError struct {
44	Info         string // the encountered info section
45	Experimental bool   // info section starts with 1
46}
47
48func (e *FlagError) Error() string {
49	if e.Experimental {
50		return "maildir: experimental info section encountered: " + e.Info[2:]
51	}
52	return "maildir: bad info section encountered: " + e.Info
53}
54
55// A MailfileError occurs when a mailfile has an invalid format
56type MailfileError struct {
57	Name string // the name of the mailfile
58}
59
60func (e *MailfileError) Error() string {
61	return "maildir: invalid mailfile format: " + e.Name
62}
63
64// A Dir represents a single directory in a Maildir mailbox.
65//
66// Dir is used by programs receiving and reading messages from a Maildir. Only
67// one process can perform these operations. Programs which only need to
68// deliver new messages to the Maildir should use Delivery.
69type Dir string
70
71// Unseen moves messages from new to cur and returns their keys.
72// This means the messages are now known to the application. To find out whether
73// a user has seen a message, use Flags().
74func (d Dir) Unseen() ([]string, error) {
75	f, err := os.Open(filepath.Join(string(d), "new"))
76	if err != nil {
77		return nil, err
78	}
79	defer f.Close()
80	names, err := f.Readdirnames(0)
81	if err != nil {
82		return nil, err
83	}
84	var keys []string
85	for _, n := range names {
86		if n[0] != '.' {
87			split := strings.FieldsFunc(n, func(r rune) bool {
88				return r == separator
89			})
90			key := split[0]
91			info := "2,"
92			// Messages in new shouldn't have an info section but
93			// we act as if, in case some other program didn't
94			// follow the spec.
95			if len(split) > 1 {
96				info = split[1]
97			}
98			keys = append(keys, key)
99			err = os.Rename(filepath.Join(string(d), "new", n),
100				filepath.Join(string(d), "cur", key+string(separator)+info))
101		}
102	}
103	return keys, err
104}
105
106// UnseenCount returns the number of messages in new without looking at them.
107func (d Dir) UnseenCount() (int, error) {
108	f, err := os.Open(filepath.Join(string(d), "new"))
109	if err != nil {
110		return 0, err
111	}
112	defer f.Close()
113	names, err := f.Readdirnames(0)
114	if err != nil {
115		return 0, err
116	}
117	c := 0
118	for _, n := range names {
119		if n[0] != '.' {
120			c += 1
121		}
122	}
123	return c, nil
124}
125
126// Keys returns a slice of valid keys to access messages by.
127func (d Dir) Keys() ([]string, error) {
128	f, err := os.Open(filepath.Join(string(d), "cur"))
129	if err != nil {
130		return nil, err
131	}
132	defer f.Close()
133	names, err := f.Readdirnames(0)
134	if err != nil {
135		return nil, err
136	}
137	var keys []string
138	for _, n := range names {
139		if n[0] != '.' {
140			split := strings.FieldsFunc(n, func(r rune) bool {
141				return r == separator
142			})
143			keys = append(keys, split[0])
144		}
145	}
146	return keys, nil
147}
148
149func (d Dir) filenameGuesses(key string) []string {
150	basename := filepath.Join(string(d), "cur", key+string(separator)+"2,")
151	return []string{
152		basename,
153
154		basename + string(FlagPassed),
155		basename + string(FlagReplied),
156		basename + string(FlagSeen),
157		basename + string(FlagDraft),
158		basename + string(FlagFlagged),
159
160		basename + string(FlagPassed),
161		basename + string(FlagPassed) + string(FlagSeen),
162		basename + string(FlagPassed) + string(FlagSeen) + string(FlagFlagged),
163		basename + string(FlagPassed) + string(FlagFlagged),
164
165		basename + string(FlagReplied) + string(FlagSeen),
166		basename + string(FlagReplied) + string(FlagSeen) + string(FlagFlagged),
167		basename + string(FlagReplied) + string(FlagFlagged),
168
169		basename + string(FlagSeen) + string(FlagFlagged),
170	}
171}
172
173// Filename returns the path to the file corresponding to the key.
174func (d Dir) Filename(key string) (string, error) {
175	// before doing an expensive Glob, see if we can guess the path based on some
176	// common flags
177	for _, guess := range d.filenameGuesses(key) {
178		if _, err := os.Stat(guess); err == nil {
179			return guess, nil
180		}
181	}
182	matches, err := filepath.Glob(filepath.Join(string(d), "cur", key+"*"))
183	if err != nil {
184		return "", err
185	}
186	if n := len(matches); n != 1 {
187		return "", &KeyError{key, n}
188	}
189	return matches[0], nil
190}
191
192// Open reads a message by key.
193func (d Dir) Open(key string) (io.ReadCloser, error) {
194	filename, err := d.Filename(key)
195	if err != nil {
196		return nil, err
197	}
198	return os.Open(filename)
199}
200
201type Flag rune
202
203const (
204	// The user has resent/forwarded/bounced this message to someone else.
205	FlagPassed Flag = 'P'
206	// The user has replied to this message.
207	FlagReplied Flag = 'R'
208	// The user has viewed this message, though perhaps he didn't read all the
209	// way through it.
210	FlagSeen Flag = 'S'
211	// The user has moved this message to the trash; the trash will be emptied
212	// by a later user action.
213	FlagTrashed Flag = 'T'
214	// The user considers this message a draft; toggled at user discretion.
215	FlagDraft Flag = 'D'
216	// User-defined flag; toggled at user discretion.
217	FlagFlagged Flag = 'F'
218)
219
220type flagList []Flag
221
222func (s flagList) Len() int           { return len(s) }
223func (s flagList) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
224func (s flagList) Less(i, j int) bool { return s[i] < s[j] }
225
226// Flags returns the flags for a message sorted in ascending order.
227// See the documentation of SetFlags for details.
228func (d Dir) Flags(key string) ([]Flag, error) {
229	filename, err := d.Filename(key)
230	if err != nil {
231		return nil, err
232	}
233	split := strings.FieldsFunc(filename, func(r rune) bool {
234		return r == separator
235	})
236	switch {
237	case len(split) <= 1:
238		return nil, &MailfileError{filename}
239	case len(split[1]) < 2,
240		split[1][1] != ',':
241		return nil, &FlagError{split[1], false}
242	case split[1][0] == '1':
243		return nil, &FlagError{split[1], true}
244	case split[1][0] != '2':
245		return nil, &FlagError{split[1], false}
246	}
247	fl := flagList(split[1][2:])
248	sort.Sort(fl)
249	return []Flag(fl), nil
250}
251
252func formatInfo(flags []Flag) string {
253	info := "2,"
254	fl := flagList(flags)
255	sort.Sort(fl)
256	for _, f := range fl {
257		if []rune(info)[len(info)-1] != rune(f) {
258			info += string(f)
259		}
260	}
261	return info
262}
263
264// SetFlags appends an info section to the filename according to the given flags.
265// This function removes duplicates and sorts the flags, but doesn't check
266// whether they conform with the Maildir specification.
267func (d Dir) SetFlags(key string, flags []Flag) error {
268	return d.SetInfo(key, formatInfo(flags))
269}
270
271// Set the info part of the filename.
272// Only use this if you plan on using a non-standard info part.
273func (d Dir) SetInfo(key, info string) error {
274	filename, err := d.Filename(key)
275	if err != nil {
276		return err
277	}
278	err = os.Rename(filename, filepath.Join(string(d), "cur", key+
279		string(separator)+info))
280	return err
281}
282
283// newKey generates a new unique key as described in the Maildir specification.
284// For the third part of the key (delivery identifier) it uses an internal
285// counter, the process id and a cryptographical random number to ensure
286// uniqueness among messages delivered in the same second.
287func newKey() (string, error) {
288	var key string
289	key += strconv.FormatInt(time.Now().Unix(), 10)
290	key += "."
291	host, err := os.Hostname()
292	if err != err {
293		return "", err
294	}
295	host = strings.Replace(host, "/", "\057", -1)
296	host = strings.Replace(host, string(separator), "\072", -1)
297	key += host
298	key += "."
299	key += strconv.FormatInt(int64(os.Getpid()), 10)
300	key += strconv.FormatInt(id, 10)
301	atomic.AddInt64(&id, 1)
302	bs := make([]byte, 10)
303	_, err = io.ReadFull(rand.Reader, bs)
304	if err != nil {
305		return "", err
306	}
307	key += hex.EncodeToString(bs)
308	return key, nil
309}
310
311// Init creates the directory structure for a Maildir.
312//
313// If the main directory already exists, it tries to create the subdirectories
314// in there. If an error occurs while creating one of the subdirectories, this
315// function may leave a partially created directory structure.
316func (d Dir) Init() error {
317	err := os.Mkdir(string(d), os.ModeDir|createMode)
318	if err != nil && !os.IsExist(err) {
319		return err
320	}
321	err = os.Mkdir(filepath.Join(string(d), "tmp"), os.ModeDir|createMode)
322	if err != nil && !os.IsExist(err) {
323		return err
324	}
325	err = os.Mkdir(filepath.Join(string(d), "new"), os.ModeDir|createMode)
326	if err != nil && !os.IsExist(err) {
327		return err
328	}
329	err = os.Mkdir(filepath.Join(string(d), "cur"), os.ModeDir|createMode)
330	if err != nil && !os.IsExist(err) {
331		return err
332	}
333	return nil
334}
335
336// Move moves a message from this Maildir to another.
337func (d Dir) Move(target Dir, key string) error {
338	path, err := d.Filename(key)
339	if err != nil {
340		return err
341	}
342	return os.Rename(path, filepath.Join(string(target), "cur", filepath.Base(path)))
343}
344
345// Copy copies the message with key from this Maildir to the target, preserving
346// its flags, returning the newly generated key for the target maildir or an
347// error.
348func (d Dir) Copy(target Dir, key string) (string, error) {
349	flags, err := d.Flags(key)
350	if err != nil {
351		return "", err
352	}
353	targetKey, err := d.copyToTmp(target, key)
354	if err != nil {
355		return "", err
356	}
357	tmpfile := filepath.Join(string(target), "tmp", targetKey)
358	curfile := filepath.Join(string(target), "cur", targetKey+"2,")
359	if err = os.Rename(tmpfile, curfile); err != nil {
360		return "", err
361	}
362	if err = target.SetFlags(targetKey, flags); err != nil {
363		return "", err
364	}
365	return targetKey, nil
366}
367
368// copyToTmp copies the message with key from d into a file in the target
369// maildir's tmp directory with a new key, returning the newly generated key or
370// an error.
371func (d Dir) copyToTmp(target Dir, key string) (string, error) {
372	rc, err := d.Open(key)
373	if err != nil {
374		return "", err
375	}
376	defer rc.Close()
377	targetKey, err := newKey()
378	if err != nil {
379		return "", err
380	}
381	tmpfile := filepath.Join(string(target), "tmp", targetKey)
382	wc, err := os.OpenFile(tmpfile, os.O_CREATE|os.O_WRONLY, 0600)
383	if err != nil {
384		return "", err
385	}
386	defer wc.Close()
387	if _, err = io.Copy(wc, rc); err != nil {
388		return "", err
389	}
390	return targetKey, nil
391}
392
393// Create inserts a new message into the Maildir.
394func (d Dir) Create(flags []Flag) (key string, w io.WriteCloser, err error) {
395	key, err = newKey()
396	if err != nil {
397		return "", nil, err
398	}
399	name := key + string(separator) + formatInfo(flags)
400	w, err = os.Create(filepath.Join(string(d), "cur", name))
401	if err != nil {
402		return "", nil, err
403	}
404	return key, w, err
405}
406
407// Remove removes the actual file behind this message.
408func (d Dir) Remove(key string) error {
409	f, err := d.Filename(key)
410	if err != nil {
411		return err
412	}
413	return os.Remove(f)
414}
415
416// Clean removes old files from tmp and should be run periodically.
417// This does not use access time but modification time for portability reasons.
418func (d Dir) Clean() error {
419	f, err := os.Open(filepath.Join(string(d), "tmp"))
420	if err != nil {
421		return err
422	}
423	defer f.Close()
424	names, err := f.Readdirnames(0)
425	if err != nil {
426		return err
427	}
428	now := time.Now()
429	for _, n := range names {
430		fi, err := os.Stat(filepath.Join(string(d), "tmp", n))
431		if err != nil {
432			continue
433		}
434		if now.Sub(fi.ModTime()).Hours() > 36 {
435			err = os.Remove(filepath.Join(string(d), "tmp", n))
436			if err != nil {
437				return err
438			}
439		}
440	}
441	return nil
442}
443
444// Delivery represents an ongoing message delivery to the mailbox. It
445// implements the io.WriteCloser interface. On Close the underlying file is
446// moved/relinked to new.
447//
448// Multiple processes can perform a delivery on the same Maildir concurrently.
449type Delivery struct {
450	file *os.File
451	d    Dir
452	key  string
453}
454
455// NewDelivery creates a new Delivery.
456func NewDelivery(d string) (*Delivery, error) {
457	key, err := newKey()
458	if err != nil {
459		return nil, err
460	}
461	del := &Delivery{}
462	file, err := os.Create(filepath.Join(d, "tmp", key))
463	if err != nil {
464		return nil, err
465	}
466	del.file = file
467	del.d = Dir(d)
468	del.key = key
469	return del, nil
470}
471
472// Write implements io.Writer.
473func (d *Delivery) Write(p []byte) (int, error) {
474	return d.file.Write(p)
475}
476
477// Close closes the underlying file and moves it to new.
478func (d *Delivery) Close() error {
479	tmppath := d.file.Name()
480	err := d.file.Close()
481	if err != nil {
482		return err
483	}
484	err = os.Link(tmppath, filepath.Join(string(d.d), "new", d.key))
485	if err != nil {
486		return err
487	}
488	err = os.Remove(tmppath)
489	if err != nil {
490		return err
491	}
492	return nil
493}
494
495// Abort closes the underlying file and removes it completely.
496func (d *Delivery) Abort() error {
497	tmppath := d.file.Name()
498	err := d.file.Close()
499	if err != nil {
500		return err
501	}
502	err = os.Remove(tmppath)
503	if err != nil {
504		return err
505	}
506	return nil
507}
508