1
0
Fork 0
mirror of https://github.com/vbatts/freezing-octo-hipster.git synced 2024-11-24 07:55:39 +00:00
random-utils/vendor/github.com/luksen/maildir/maildir.go
Vincent Batts 4ab3be9bc6
go*: one go module for the repo, no more nested
Signed-off-by: Vincent Batts <vbatts@hashbangbash.com>
2024-04-26 19:34:55 +00:00

520 lines
12 KiB
Go

// The maildir package provides an interface to mailboxes in the Maildir format.
package maildir
import (
"bufio"
"bytes"
"crypto/rand"
"encoding/hex"
"fmt"
"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
}
// 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.
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 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 ""
}
// Filename returns the path to the file corresponding to the key.
func (d Dir) Filename(key string) (string, error) {
if matchedFile := d.quickFilename(key); matchedFile != "" {
return matchedFile, nil
}
dirPath := filepath.Join(string(d), "cur")
f, err := os.Open(dirPath)
if err != nil {
return "", err
}
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
}
}
return "", &KeyError{key, 0}
}
// 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.
//
// Using only these standard flags will improve message retrieval speed.
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
}
// Key exposes the internal unique key generation. This function is deprecated.
func Key() (string, error) {
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) {
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) {
key, err := generateKey()
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
}