2019-04-09 13:19:08 +00:00
|
|
|
// The maildir package provides an interface to mailboxes in the Maildir format.
|
|
|
|
package maildir
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"bytes"
|
|
|
|
"crypto/rand"
|
|
|
|
"encoding/hex"
|
2024-04-26 19:34:55 +00:00
|
|
|
"fmt"
|
2019-04-09 13:19:08 +00:00
|
|
|
"io"
|
|
|
|
"net/mail"
|
|
|
|
"net/textproto"
|
|
|
|
"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 Dir represents a single directory in a Maildir mailbox.
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-04-26 19:34:55 +00:00
|
|
|
// Keys returns a slice of valid keys to access messages by. This only returns
|
|
|
|
// keys for messages in cur. Use Unseen to access messages in new. All keys,
|
|
|
|
// whether returned here or by Unseen, point to messages in cur.
|
2019-04-09 13:19:08 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-04-26 19:34:55 +00:00
|
|
|
func fileExists(filename string) bool {
|
|
|
|
finfo, err := os.Stat(filename)
|
|
|
|
return err == nil && finfo.Mode().IsRegular()
|
|
|
|
}
|
|
|
|
|
|
|
|
var suffices []string = []string{
|
|
|
|
"",
|
|
|
|
"S",
|
|
|
|
"D",
|
|
|
|
"F",
|
|
|
|
"P",
|
|
|
|
"R",
|
|
|
|
"T",
|
|
|
|
"DF",
|
|
|
|
"DP",
|
|
|
|
"DR",
|
|
|
|
"DS",
|
|
|
|
"DT",
|
|
|
|
"FP",
|
|
|
|
"FR",
|
|
|
|
"FS",
|
|
|
|
"FT",
|
|
|
|
"PR",
|
|
|
|
"PS",
|
|
|
|
"PT",
|
|
|
|
"RS",
|
|
|
|
"RT",
|
|
|
|
"ST",
|
|
|
|
"DFP",
|
|
|
|
"DFR",
|
|
|
|
"DFS",
|
|
|
|
"DFT",
|
|
|
|
"DPR",
|
|
|
|
"DPS",
|
|
|
|
"DPT",
|
|
|
|
"DRS",
|
|
|
|
"DRT",
|
|
|
|
"DST",
|
|
|
|
"FPR",
|
|
|
|
"FPS",
|
|
|
|
"FPT",
|
|
|
|
"FRS",
|
|
|
|
"FRT",
|
|
|
|
"FST",
|
|
|
|
"PRS",
|
|
|
|
"PRT",
|
|
|
|
"PST",
|
|
|
|
"RST",
|
|
|
|
"DFPR",
|
|
|
|
"DFPS",
|
|
|
|
"DFPT",
|
|
|
|
"DFRS",
|
|
|
|
"DFRT",
|
|
|
|
"DFST",
|
|
|
|
"DPRS",
|
|
|
|
"DPRT",
|
|
|
|
"DPST",
|
|
|
|
"DRST",
|
|
|
|
"FPRS",
|
|
|
|
"FPRT",
|
|
|
|
"FPST",
|
|
|
|
"FRST",
|
|
|
|
"PRST",
|
|
|
|
"DFPRS",
|
|
|
|
"DFPRT",
|
|
|
|
"DFPST",
|
|
|
|
"DFRST",
|
|
|
|
"DPRST",
|
|
|
|
"FPRST",
|
|
|
|
"DFPRST"}
|
|
|
|
|
|
|
|
// quick check for existance of legal (per DJB) file names for a key
|
|
|
|
func (d Dir) quickFilename(key string) string {
|
|
|
|
// "cur" files must have :info suffix
|
|
|
|
filePrefix := filepath.Join(string(d), "cur", key+string(Separator)+"2,")
|
|
|
|
for _, suffix := range suffices {
|
|
|
|
if filename := filePrefix + suffix; fileExists(filename) {
|
|
|
|
return filename
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2019-04-09 13:19:08 +00:00
|
|
|
// Filename returns the path to the file corresponding to the key.
|
|
|
|
func (d Dir) Filename(key string) (string, error) {
|
2024-04-26 19:34:55 +00:00
|
|
|
if matchedFile := d.quickFilename(key); matchedFile != "" {
|
|
|
|
return matchedFile, nil
|
|
|
|
}
|
|
|
|
dirPath := filepath.Join(string(d), "cur")
|
|
|
|
f, err := os.Open(dirPath)
|
2019-04-09 13:19:08 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2024-04-26 19:34:55 +00:00
|
|
|
defer f.Close()
|
|
|
|
names, err := f.Readdirnames(-1)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
for _, name := range names {
|
|
|
|
if strings.HasPrefix(name, key) {
|
|
|
|
return filepath.Join(dirPath, name), nil
|
|
|
|
}
|
2019-04-09 13:19:08 +00:00
|
|
|
}
|
2024-04-26 19:34:55 +00:00
|
|
|
return "", &KeyError{key, 0}
|
2019-04-09 13:19:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Header returns the corresponding mail header to a key.
|
|
|
|
func (d Dir) Header(key string) (header mail.Header, err error) {
|
|
|
|
filename, err := d.Filename(key)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
file, err := os.Open(filename)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
tp := textproto.NewReader(bufio.NewReader(file))
|
|
|
|
hdr, err := tp.ReadMIMEHeader()
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
header = mail.Header(hdr)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Message returns a Message by key.
|
|
|
|
func (d Dir) Message(key string) (*mail.Message, error) {
|
|
|
|
filename, err := d.Filename(key)
|
|
|
|
if err != nil {
|
|
|
|
return &mail.Message{}, err
|
|
|
|
}
|
|
|
|
r, err := os.Open(filename)
|
|
|
|
if err != nil {
|
|
|
|
return &mail.Message{}, err
|
|
|
|
}
|
|
|
|
defer r.Close()
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
_, err = io.Copy(buf, r)
|
|
|
|
if err != nil {
|
|
|
|
return &mail.Message{}, err
|
|
|
|
}
|
|
|
|
msg, err := mail.ReadMessage(buf)
|
|
|
|
if err != nil {
|
|
|
|
return msg, err
|
|
|
|
}
|
|
|
|
return msg, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type runeSlice []rune
|
|
|
|
|
|
|
|
func (s runeSlice) Len() int { return len(s) }
|
|
|
|
func (s runeSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
|
|
|
func (s runeSlice) 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) (string, error) {
|
|
|
|
filename, err := d.Filename(key)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
split := strings.FieldsFunc(filename, func(r rune) bool {
|
|
|
|
return r == Separator
|
|
|
|
})
|
|
|
|
switch {
|
|
|
|
case len(split[1]) < 2,
|
|
|
|
split[1][1] != ',':
|
|
|
|
return "", &FlagError{split[1], false}
|
|
|
|
case split[1][0] == '1':
|
|
|
|
return "", &FlagError{split[1], true}
|
|
|
|
case split[1][0] != '2':
|
|
|
|
return "", &FlagError{split[1], false}
|
|
|
|
}
|
|
|
|
rs := runeSlice(split[1][2:])
|
|
|
|
sort.Sort(rs)
|
|
|
|
return string(rs), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
//
|
|
|
|
// The following flags are listed in the specification
|
|
|
|
// (http://cr.yp.to/proto/maildir.html):
|
|
|
|
//
|
|
|
|
// Flag "P" (passed): the user has resent/forwarded/bounced this message to someone else.
|
|
|
|
// Flag "R" (replied): the user has replied to this message.
|
|
|
|
// Flag "S" (seen): the user has viewed this message, though perhaps he didn't read all the way through it.
|
|
|
|
// Flag "T" (trashed): the user has moved this message to the trash; the trash will be emptied by a later user action.
|
|
|
|
// Flag "D" (draft): the user considers this message a draft; toggled at user discretion.
|
|
|
|
// Flag "F" (flagged): user-defined flag; toggled at user discretion.
|
2024-04-26 19:34:55 +00:00
|
|
|
//
|
|
|
|
// Using only these standard flags will improve message retrieval speed.
|
2019-04-09 13:19:08 +00:00
|
|
|
func (d Dir) SetFlags(key string, flags string) error {
|
|
|
|
info := "2,"
|
|
|
|
rs := runeSlice(flags)
|
|
|
|
sort.Sort(rs)
|
|
|
|
for _, r := range rs {
|
|
|
|
if []rune(info)[len(info)-1] != r {
|
|
|
|
info += string(r)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return d.SetInfo(key, info)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2024-04-26 19:34:55 +00:00
|
|
|
// Key exposes the internal unique key generation. This function is deprecated.
|
2019-04-09 13:19:08 +00:00
|
|
|
func Key() (string, error) {
|
2024-04-26 19:34:55 +00:00
|
|
|
fmt.Println("maildir: Key() is deprecated without replacement. See https://github.com/luksen/maildir/issues/5 for details.")
|
|
|
|
return generateKey()
|
|
|
|
}
|
|
|
|
|
|
|
|
// generateKey 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 generateKey() (string, error) {
|
2019-04-09 13:19:08 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create 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) Create() 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
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delivery represents an ongoing message delivery to the mailbox.
|
|
|
|
// It implements the WriteCloser interface. On closing the underlying file is
|
|
|
|
// moved/relinked to new.
|
|
|
|
type Delivery struct {
|
|
|
|
file *os.File
|
|
|
|
d Dir
|
|
|
|
key string
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewDelivery creates a new Delivery.
|
|
|
|
func (d Dir) NewDelivery() (*Delivery, error) {
|
2024-04-26 19:34:55 +00:00
|
|
|
key, err := generateKey()
|
2019-04-09 13:19:08 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
del := &Delivery{}
|
|
|
|
time.AfterFunc(24*time.Hour, func() { del.Abort() })
|
|
|
|
file, err := os.Create(filepath.Join(string(d), "tmp", key))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
del.file = file
|
|
|
|
del.d = d
|
|
|
|
del.key = key
|
|
|
|
return del, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
|
|
|
err := d.file.Close()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
err = os.Link(filepath.Join(string(d.d), "tmp", d.key),
|
|
|
|
filepath.Join(string(d.d), "new", d.key))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
err = os.Remove(filepath.Join(string(d.d), "tmp", d.key))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Abort closes the underlying file and removes it completely.
|
|
|
|
func (d *Delivery) Abort() error {
|
|
|
|
err := d.file.Close()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
err = os.Remove(filepath.Join(string(d.d), "tmp", d.key))
|
|
|
|
if err != nil {
|
|
|
|
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)))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Purge removes the actual file behind this message.
|
|
|
|
func (d Dir) Purge(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
|
|
|
|
}
|