pax_global_header00006660000000000000000000000064136267104370014523gustar00rootroot0000000000000052 comment=a05813beeb4a3b6a0cbddbc688e5ad63d918b9fd go-maildir-0.2.0/000077500000000000000000000000001362671043700135465ustar00rootroot00000000000000go-maildir-0.2.0/LICENSE000066400000000000000000000021241362671043700145520ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2014 Lukas Senger Copyright (c) 2019 Simon Ser Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. go-maildir-0.2.0/README.md000066400000000000000000000003441362671043700150260ustar00rootroot00000000000000# go-maildir [![GoDoc](https://godoc.org/github.com/emersion/go-maildir?status.svg)](https://godoc.org/github.com/emersion/go-maildir) A Go library for [maildir]. # License MIT [maildir]: http://cr.yp.to/proto/maildir.html go-maildir-0.2.0/go.mod000066400000000000000000000000571362671043700146560ustar00rootroot00000000000000module github.com/emersion/go-maildir go 1.12 go-maildir-0.2.0/maildir.go000066400000000000000000000322061362671043700155210ustar00rootroot00000000000000// The maildir package provides an interface to mailboxes in the Maildir format. // // Maildir mailboxes are designed to be safe for concurrent delivery. This // means that at the same time, multiple processes can deliver to the same // mailbox. However only one process can receive and read messages stored in // the Maildir. package maildir import ( "crypto/rand" "encoding/hex" "io" "os" "path/filepath" "sort" "strconv" "strings" "sync/atomic" "time" ) // The separator separates a messages unique key from its flags in the filename. // This should only be changed on operating systems where the colon isn't // allowed in filenames. var separator rune = ':' var id int64 = 10000 // createMode holds the permissions used when creating a directory. const createMode = 0700 // A KeyError occurs when a key matches more or less than one message. type KeyError struct { Key string // the (invalid) key N int // number of matches (!= 1) } func (e *KeyError) Error() string { return "maildir: key " + e.Key + " matches " + strconv.Itoa(e.N) + " files." } // A FlagError occurs when a non-standard info section is encountered. type FlagError struct { Info string // the encountered info section Experimental bool // info section starts with 1 } func (e *FlagError) Error() string { if e.Experimental { return "maildir: experimental info section encountered: " + e.Info[2:] } return "maildir: bad info section encountered: " + e.Info } // A MailfileError occurs when a mailfile has an invalid format type MailfileError struct { Name string // the name of the mailfile } func (e *MailfileError) Error() string { return "maildir: invalid mailfile format: " + e.Name } // A Dir represents a single directory in a Maildir mailbox. // // Dir is used by programs receiving and reading messages from a Maildir. Only // one process can perform these operations. Programs which only need to // deliver new messages to the Maildir should use Delivery. type Dir string // Unseen moves messages from new to cur and returns their keys. // This means the messages are now known to the application. To find out whether // a user has seen a message, use Flags(). func (d Dir) Unseen() ([]string, error) { f, err := os.Open(filepath.Join(string(d), "new")) if err != nil { return nil, err } defer f.Close() names, err := f.Readdirnames(0) if err != nil { return nil, err } var keys []string for _, n := range names { if n[0] != '.' { split := strings.FieldsFunc(n, func(r rune) bool { return r == separator }) key := split[0] info := "2," // Messages in new shouldn't have an info section but // we act as if, in case some other program didn't // follow the spec. if len(split) > 1 { info = split[1] } keys = append(keys, key) err = os.Rename(filepath.Join(string(d), "new", n), filepath.Join(string(d), "cur", key+string(separator)+info)) } } return keys, err } // UnseenCount returns the number of messages in new without looking at them. func (d Dir) UnseenCount() (int, error) { f, err := os.Open(filepath.Join(string(d), "new")) if err != nil { return 0, err } defer f.Close() names, err := f.Readdirnames(0) if err != nil { return 0, err } c := 0 for _, n := range names { if n[0] != '.' { c += 1 } } return c, nil } // Keys returns a slice of valid keys to access messages by. func (d Dir) Keys() ([]string, error) { f, err := os.Open(filepath.Join(string(d), "cur")) if err != nil { return nil, err } defer f.Close() names, err := f.Readdirnames(0) if err != nil { return nil, err } var keys []string for _, n := range names { if n[0] != '.' { split := strings.FieldsFunc(n, func(r rune) bool { return r == separator }) keys = append(keys, split[0]) } } return keys, nil } func (d Dir) filenameGuesses(key string) []string { basename := filepath.Join(string(d), "cur", key+string(separator)+"2,") return []string{ basename, basename + string(FlagPassed), basename + string(FlagReplied), basename + string(FlagSeen), basename + string(FlagDraft), basename + string(FlagFlagged), basename + string(FlagPassed), basename + string(FlagPassed) + string(FlagSeen), basename + string(FlagPassed) + string(FlagSeen) + string(FlagFlagged), basename + string(FlagPassed) + string(FlagFlagged), basename + string(FlagReplied) + string(FlagSeen), basename + string(FlagReplied) + string(FlagSeen) + string(FlagFlagged), basename + string(FlagReplied) + string(FlagFlagged), basename + string(FlagSeen) + string(FlagFlagged), } } // Filename returns the path to the file corresponding to the key. func (d Dir) Filename(key string) (string, error) { // before doing an expensive Glob, see if we can guess the path based on some // common flags for _, guess := range d.filenameGuesses(key) { if _, err := os.Stat(guess); err == nil { return guess, nil } } matches, err := filepath.Glob(filepath.Join(string(d), "cur", key+"*")) if err != nil { return "", err } if n := len(matches); n != 1 { return "", &KeyError{key, n} } return matches[0], nil } // Open reads a message by key. func (d Dir) Open(key string) (io.ReadCloser, error) { filename, err := d.Filename(key) if err != nil { return nil, err } return os.Open(filename) } type Flag rune const ( // The user has resent/forwarded/bounced this message to someone else. FlagPassed Flag = 'P' // The user has replied to this message. FlagReplied Flag = 'R' // The user has viewed this message, though perhaps he didn't read all the // way through it. FlagSeen Flag = 'S' // The user has moved this message to the trash; the trash will be emptied // by a later user action. FlagTrashed Flag = 'T' // The user considers this message a draft; toggled at user discretion. FlagDraft Flag = 'D' // User-defined flag; toggled at user discretion. FlagFlagged Flag = 'F' ) type flagList []Flag func (s flagList) Len() int { return len(s) } func (s flagList) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s flagList) Less(i, j int) bool { return s[i] < s[j] } // Flags returns the flags for a message sorted in ascending order. // See the documentation of SetFlags for details. func (d Dir) Flags(key string) ([]Flag, error) { filename, err := d.Filename(key) if err != nil { return nil, err } split := strings.FieldsFunc(filename, func(r rune) bool { return r == separator }) switch { case len(split) <= 1: return nil, &MailfileError{filename} case len(split[1]) < 2, split[1][1] != ',': return nil, &FlagError{split[1], false} case split[1][0] == '1': return nil, &FlagError{split[1], true} case split[1][0] != '2': return nil, &FlagError{split[1], false} } fl := flagList(split[1][2:]) sort.Sort(fl) return []Flag(fl), nil } func formatInfo(flags []Flag) string { info := "2," fl := flagList(flags) sort.Sort(fl) for _, f := range fl { if []rune(info)[len(info)-1] != rune(f) { info += string(f) } } return info } // SetFlags appends an info section to the filename according to the given flags. // This function removes duplicates and sorts the flags, but doesn't check // whether they conform with the Maildir specification. func (d Dir) SetFlags(key string, flags []Flag) error { return d.SetInfo(key, formatInfo(flags)) } // Set the info part of the filename. // Only use this if you plan on using a non-standard info part. func (d Dir) SetInfo(key, info string) error { filename, err := d.Filename(key) if err != nil { return err } err = os.Rename(filename, filepath.Join(string(d), "cur", key+ string(separator)+info)) return err } // newKey generates a new unique key as described in the Maildir specification. // For the third part of the key (delivery identifier) it uses an internal // counter, the process id and a cryptographical random number to ensure // uniqueness among messages delivered in the same second. func newKey() (string, error) { var key string key += strconv.FormatInt(time.Now().Unix(), 10) key += "." host, err := os.Hostname() if err != err { return "", err } host = strings.Replace(host, "/", "\057", -1) host = strings.Replace(host, string(separator), "\072", -1) key += host key += "." key += strconv.FormatInt(int64(os.Getpid()), 10) key += strconv.FormatInt(id, 10) atomic.AddInt64(&id, 1) bs := make([]byte, 10) _, err = io.ReadFull(rand.Reader, bs) if err != nil { return "", err } key += hex.EncodeToString(bs) return key, nil } // Init creates the directory structure for a Maildir. // // If the main directory already exists, it tries to create the subdirectories // in there. If an error occurs while creating one of the subdirectories, this // function may leave a partially created directory structure. func (d Dir) Init() error { err := os.Mkdir(string(d), os.ModeDir|createMode) if err != nil && !os.IsExist(err) { return err } err = os.Mkdir(filepath.Join(string(d), "tmp"), os.ModeDir|createMode) if err != nil && !os.IsExist(err) { return err } err = os.Mkdir(filepath.Join(string(d), "new"), os.ModeDir|createMode) if err != nil && !os.IsExist(err) { return err } err = os.Mkdir(filepath.Join(string(d), "cur"), os.ModeDir|createMode) if err != nil && !os.IsExist(err) { return err } return nil } // Move moves a message from this Maildir to another. func (d Dir) Move(target Dir, key string) error { path, err := d.Filename(key) if err != nil { return err } return os.Rename(path, filepath.Join(string(target), "cur", filepath.Base(path))) } // Copy copies the message with key from this Maildir to the target, preserving // its flags, returning the newly generated key for the target maildir or an // error. func (d Dir) Copy(target Dir, key string) (string, error) { flags, err := d.Flags(key) if err != nil { return "", err } targetKey, err := d.copyToTmp(target, key) if err != nil { return "", err } tmpfile := filepath.Join(string(target), "tmp", targetKey) curfile := filepath.Join(string(target), "cur", targetKey+"2,") if err = os.Rename(tmpfile, curfile); err != nil { return "", err } if err = target.SetFlags(targetKey, flags); err != nil { return "", err } return targetKey, nil } // copyToTmp copies the message with key from d into a file in the target // maildir's tmp directory with a new key, returning the newly generated key or // an error. func (d Dir) copyToTmp(target Dir, key string) (string, error) { rc, err := d.Open(key) if err != nil { return "", err } defer rc.Close() targetKey, err := newKey() if err != nil { return "", err } tmpfile := filepath.Join(string(target), "tmp", targetKey) wc, err := os.OpenFile(tmpfile, os.O_CREATE|os.O_WRONLY, 0600) if err != nil { return "", err } defer wc.Close() if _, err = io.Copy(wc, rc); err != nil { return "", err } return targetKey, nil } // Create inserts a new message into the Maildir. func (d Dir) Create(flags []Flag) (key string, w io.WriteCloser, err error) { key, err = newKey() if err != nil { return "", nil, err } name := key + string(separator) + formatInfo(flags) w, err = os.Create(filepath.Join(string(d), "cur", name)) if err != nil { return "", nil, err } return key, w, err } // Remove removes the actual file behind this message. func (d Dir) Remove(key string) error { f, err := d.Filename(key) if err != nil { return err } return os.Remove(f) } // Clean removes old files from tmp and should be run periodically. // This does not use access time but modification time for portability reasons. func (d Dir) Clean() error { f, err := os.Open(filepath.Join(string(d), "tmp")) if err != nil { return err } defer f.Close() names, err := f.Readdirnames(0) if err != nil { return err } now := time.Now() for _, n := range names { fi, err := os.Stat(filepath.Join(string(d), "tmp", n)) if err != nil { continue } if now.Sub(fi.ModTime()).Hours() > 36 { err = os.Remove(filepath.Join(string(d), "tmp", n)) if err != nil { return err } } } return nil } // Delivery represents an ongoing message delivery to the mailbox. It // implements the io.WriteCloser interface. On Close the underlying file is // moved/relinked to new. // // Multiple processes can perform a delivery on the same Maildir concurrently. type Delivery struct { file *os.File d Dir key string } // NewDelivery creates a new Delivery. func NewDelivery(d string) (*Delivery, error) { key, err := newKey() if err != nil { return nil, err } del := &Delivery{} file, err := os.Create(filepath.Join(d, "tmp", key)) if err != nil { return nil, err } del.file = file del.d = Dir(d) del.key = key return del, nil } // Write implements io.Writer. func (d *Delivery) Write(p []byte) (int, error) { return d.file.Write(p) } // Close closes the underlying file and moves it to new. func (d *Delivery) Close() error { tmppath := d.file.Name() err := d.file.Close() if err != nil { return err } err = os.Link(tmppath, filepath.Join(string(d.d), "new", d.key)) if err != nil { return err } err = os.Remove(tmppath) if err != nil { return err } return nil } // Abort closes the underlying file and removes it completely. func (d *Delivery) Abort() error { tmppath := d.file.Name() err := d.file.Close() if err != nil { return err } err = os.Remove(tmppath) if err != nil { return err } return nil } go-maildir-0.2.0/maildir_test.go000066400000000000000000000153731362671043700165660ustar00rootroot00000000000000package maildir import ( "fmt" "io" "io/ioutil" "math/rand" "os" "testing" ) // cleanup removes a Dir's directory structure func cleanup(tb testing.TB, d Dir) { err := os.RemoveAll(string(d)) if err != nil { tb.Error(err) } } // exists checks if the given path exists func exists(path string) bool { _, err := os.Stat(path) if err == nil { return true } if os.IsNotExist(err) { return false } panic(err) } // cat returns the content of a file as a string func cat(t *testing.T, path string) string { f, err := os.Open(path) if err != nil { t.Fatal(err) } defer f.Close() c, err := ioutil.ReadAll(f) if err != nil { t.Fatal(err) } return string(c) } // makeDelivery creates a new message func makeDelivery(tb testing.TB, d Dir, msg string) { del, err := NewDelivery(string(d)) if err != nil { tb.Fatal(err) } _, err = del.Write([]byte(msg)) if err != nil { tb.Fatal(err) } err = del.Close() if err != nil { tb.Fatal(err) } } func TestInit(t *testing.T) { t.Parallel() var d Dir = "test_create" err := d.Init() if err != nil { t.Fatal(err) } f, err := os.Open("test_create") if err != nil { t.Fatal(err) } fis, err := f.Readdir(0) subdirs := make(map[string]os.FileInfo) for _, fi := range fis { if !fi.IsDir() { t.Errorf("%s was not a directory", fi.Name()) continue } subdirs[fi.Name()] = fi } // Verify the directories have been created. if _, ok := subdirs["tmp"]; !ok { t.Error("'tmp' directory was not created") } if _, ok := subdirs["new"]; !ok { t.Error("'new' directory was not created") } if _, ok := subdirs["cur"]; !ok { t.Error("'cur' directory was not created") } // Make sure no error is returned if the directories already exist. err = d.Init() if err != nil { t.Fatal(err) } defer cleanup(t, d) } func TestDelivery(t *testing.T) { t.Parallel() var d Dir = "test_delivery" err := d.Init() if err != nil { t.Fatal(err) } defer cleanup(t, d) var msg = "this is a message" makeDelivery(t, d, msg) keys, err := d.Unseen() if err != nil { t.Fatal(err) } path, err := d.Filename(keys[0]) if err != nil { t.Fatal(err) } if !exists(path) { t.Fatal("File doesn't exist") } if cat(t, path) != msg { t.Fatal("Content doesn't match") } } func TestDir_Create(t *testing.T) { t.Parallel() var d Dir = "test_create" err := d.Init() if err != nil { t.Fatal(err) } defer cleanup(t, d) var msg = "this is a message" key, w, err := d.Create([]Flag{FlagFlagged}) if err != nil { t.Fatal(err) } defer w.Close() if _, err := io.WriteString(w, msg); err != nil { t.Fatal(err) } if err := w.Close(); err != nil { t.Fatal(err) } flags, err := d.Flags(key) if err != nil { t.Fatal(err) } else if len(flags) != 1 || flags[0] != FlagFlagged { t.Errorf("Dir.Flags() = %v, want {FlagFlagged}", flags) } path, err := d.Filename(key) if err != nil { t.Fatal(err) } if !exists(path) { t.Fatal("File doesn't exist") } if cat(t, path) != msg { t.Fatal("Content doesn't match") } } func TestPurge(t *testing.T) { t.Parallel() var d Dir = "test_purge" err := d.Init() if err != nil { t.Fatal(err) } defer cleanup(t, d) makeDelivery(t, d, "foo") keys, err := d.Unseen() if err != nil { t.Fatal(err) } path, err := d.Filename(keys[0]) if err != nil { t.Fatal(err) } err = d.Remove(keys[0]) if err != nil { t.Fatal(err) } if exists(path) { t.Fatal("File still exists") } } func TestMove(t *testing.T) { t.Parallel() var d1 Dir = "test_move1" err := d1.Init() if err != nil { t.Fatal(err) } defer cleanup(t, d1) var d2 Dir = "test_move2" err = d2.Init() if err != nil { t.Fatal(err) } defer cleanup(t, d2) const msg = "a moving message" makeDelivery(t, d1, msg) keys, err := d1.Unseen() if err != nil { t.Fatal(err) } err = d1.Move(d2, keys[0]) if err != nil { t.Fatal(err) } keys, err = d2.Keys() if err != nil { t.Fatal(err) } path, err := d2.Filename(keys[0]) if err != nil { t.Fatal(err) } if cat(t, path) != msg { t.Fatal("Content doesn't match") } } func TestCopy(t *testing.T) { t.Parallel() var d1 Dir = "test_copy1" err := d1.Init() if err != nil { t.Fatal(err) } defer cleanup(t, d1) var d2 Dir = "test_copy2" err = d2.Init() if err != nil { t.Fatal(err) } defer cleanup(t, d2) const msg = "a moving message" makeDelivery(t, d1, msg) keys, err := d1.Unseen() if err != nil { t.Fatal(err) } if err = d1.SetFlags(keys[0], []Flag{FlagSeen}); err != nil { t.Fatal(err) } key2, err := d1.Copy(d2, keys[0]) if err != nil { t.Fatal(err) } path, err := d1.Filename(keys[0]) if err != nil { t.Fatal(err) } if cat(t, path) != msg { t.Error("original content has changed") } path, err = d2.Filename(key2) if err != nil { t.Fatal(err) } if cat(t, path) != msg { t.Error("target content doesn't match source") } flags, err := d2.Flags(key2) if err != nil { t.Fatal(err) } if len(flags) != 1 { t.Fatal("no flags on target") } if flags[0] != FlagSeen { t.Error("seen flag not present on target") } } func TestIllegal(t *testing.T) { t.Parallel() var d1 Dir = "test_illegal" err := d1.Init() if err != nil { t.Fatal(err) } defer cleanup(t, d1) const msg = "an illegal message" makeDelivery(t, d1, msg) keys, err := d1.Unseen() if err != nil { t.Fatal(err) } if err = d1.SetFlags(keys[0], []Flag{FlagSeen}); err != nil { t.Fatal(err) } path, err := d1.Filename(keys[0]) if err != nil { t.Fatal(err) } os.Rename(path, "test_illegal/cur/"+keys[0]) _, err = d1.Flags(keys[0]) if err != nil { if _, ok := err.(*MailfileError); !ok { t.Fatal(err) } } } func BenchmarkFilename(b *testing.B) { // set up test maildir d := Dir("benchmark_filename") if err := d.Init(); err != nil { b.Fatalf("could not set up benchmark: %v", err) } defer cleanup(b, d) // make 5000 deliveries for i := 0; i < 5000; i++ { makeDelivery(b, d, fmt.Sprintf("here is message number %d", i)) } // grab keys keys, err := d.Unseen() if err != nil { b.Fatal(err) } // shuffle keys rand.Shuffle(len(keys), func(i, j int) { keys[i], keys[j] = keys[j], keys[i] }) // set some flags for i, key := range keys { var err error switch i % 5 { case 0: // no flags fallthrough case 1: err = d.SetFlags(key, []Flag{FlagSeen}) case 2: err = d.SetFlags(key, []Flag{FlagSeen, FlagReplied}) case 3: err = d.SetFlags(key, []Flag{FlagReplied}) case 4: err = d.SetFlags(key, []Flag{FlagFlagged}) } if err != nil { b.Fatal(err) } } // run benchmark for the first N shuffled keys keyIdx := 0 b.ResetTimer() for i := 0; i < b.N; i++ { b.StartTimer() _, err := d.Filename(keys[keyIdx]) b.StopTimer() if err != nil { b.Errorf("could not get filename for key %s", keys[keyIdx]) } keyIdx++ if keyIdx >= len(keys) { keyIdx = 0 } } } go-maildir-0.2.0/maildirpp/000077500000000000000000000000001362671043700155275ustar00rootroot00000000000000go-maildir-0.2.0/maildirpp/maildirpp.go000066400000000000000000000010751362671043700200420ustar00rootroot00000000000000// Package maildirpp implements Maildir++. package maildirpp import ( "errors" "strings" ) const separator = '.' func Split(key string) ([]string, error) { if len(key) == 0 || key[0] != '.' { return nil, errors.New("maildirpp: invalid key") } return strings.Split(key, string(separator))[1:], nil } func Join(elems []string) (key string, err error) { for _, d := range elems { if strings.ContainsRune(d, separator) { return "", errors.New("maildirpp: directory name cannot contain a dot") } } return "." + strings.Join(elems, string(separator)), nil }