papero/fs/maildir.go

227 lines
4.8 KiB
Go

package fs
import (
"crypto/md5"
"errors"
"fmt"
"os"
"path"
"sort"
"strconv"
"strings"
"time"
)
var ErrPathIsNotDirectory = errors.New("the path is not a directory")
var ErrMalformedName = errors.New("file name is not recognised")
var ErrUnknownFlag = errors.New("flag not known")
type Flag string
func cat(f []Flag) string {
sort.Slice(f, func(i, j int) bool { return f[i] < f[j] })
var list []string
for _, el := range f {
list = append(list, string(el))
}
return strings.Join(list, "")
}
const (
FlagSeen Flag = "S"
FlagAnswered = "R"
FlagFlagged = "F"
FlagDeleted = "T"
FlagDraft = "D"
FlagUnknown = ""
)
func flagFromChar(char rune) (Flag, error) {
switch string(char) {
case "S":
return FlagSeen, nil
case "R":
return FlagAnswered, nil
case "F":
return FlagFlagged, nil
case "T":
return FlagDeleted, nil
case "D":
return FlagDraft, nil
default:
return FlagUnknown, ErrUnknownFlag
}
}
func flagsFromString(flags string) ([]Flag, error) {
result := []Flag{}
knownFlags := map[Flag]bool{
FlagSeen: true,
FlagAnswered: true,
FlagFlagged: true,
FlagDeleted: true,
FlagDraft: true,
}
if flags == "" {
return result, nil
}
for _, f := range flags {
if flag, err := flagFromChar(f); err != nil {
return []Flag{}, err
} else {
if knownFlags[flag] {
result = append(result, flag)
knownFlags[flag] = false
} else {
return []Flag{}, ErrMalformedName
}
}
}
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
return result, nil
}
// MailFile holds the information needed to format the name of the file
// containing a single email. The format is:
// `<%d_%d.%d.%s>,U=<%d>,FMD5=<%s>:2,<FLAGS>`
// (logical groups being enclosed by angle brackets) where:
// - `<%d_%d.%d.%s>` is the concatenation of:
// * system unix timestamp of arrival time of the message
// * a progressive number to distinguish messages arrived at once
// * pid of the current process
// * hostname of the local machine
// - `,U=<%d>` holds the UID of the message as decided by the server
// - `,FMD5=<%s>:2` holds the md5sum of the name of the current mailbox
// - `,<FLAGS>` carries the flags active on the message
type MailFile struct {
timestamp time.Time
progressive int
pid int
hostname string
uid int
md5 string
flags []Flag
}
func (m *MailFile) SetUid(uid int) {
m.uid = uid
}
func (m *MailFile) SetMd5(md5sum string) {
m.md5 = md5sum
}
func (m *MailFile) SetMd5FromName(name string) {
md5sum := md5.Sum([]byte(name))
m.SetMd5(fmt.Sprintf("%x", md5sum))
}
func (m *MailFile) SetFlags(flags []Flag) {
sort.Slice(flags, func(i, j int) bool { return flags[i] < flags[j] })
m.flags = flags
}
func (m *MailFile) String() string {
return fmt.Sprintf(
"%d_%d.%d.%s,U=%d,FMD5=%s:2,%s",
m.timestamp.Unix(),
m.progressive,
m.pid,
m.hostname,
m.uid,
m.md5,
cat(m.flags),
)
}
func NewMailFile(name string) (*MailFile, error) {
mailFile := &MailFile{}
parts := strings.Split(name, ",")
if len(parts) != 4 {
return mailFile, ErrMalformedName
}
sub1 := strings.Split(parts[0], ".")
if len(sub1) != 3 {
return mailFile, ErrMalformedName
}
sub2 := strings.Split(sub1[0], "_")
if len(sub2) != 2 {
return mailFile, ErrMalformedName
}
unix, err := strconv.ParseInt(sub2[0], 10, 64)
if err != nil {
return mailFile, err
}
mailFile.timestamp = time.Unix(unix, 0)
progressive, err := strconv.ParseInt(sub2[1], 10, 32)
if err != nil {
return mailFile, err
}
mailFile.progressive = int(progressive)
pid, err := strconv.ParseInt(sub1[1], 10, 32)
if err != nil {
return mailFile, err
}
mailFile.pid = int(pid)
mailFile.hostname = sub1[2]
sub3 := strings.Split(parts[1], "=")
if len(sub3) != 2 || sub3[0] != "U" {
return mailFile, ErrMalformedName
}
uid, err := strconv.ParseInt(sub3[1], 10, 32)
if err != nil {
return mailFile, err
}
mailFile.uid = int(uid)
sub4 := strings.Split(parts[2], "=")
if len(sub4) != 2 || sub4[0] != "FMD5" {
return mailFile, ErrMalformedName
}
sub5 := strings.Split(sub4[1], ":")
if len(sub5) != 2 || sub5[1] != "2" {
return mailFile, ErrMalformedName
}
mailFile.md5 = sub5[0]
flags, err := flagsFromString(parts[3])
if err != nil {
return mailFile, err
}
mailFile.flags = flags
return mailFile, nil
}
type MailDir struct {
basePath string
name string
MailFiles []MailFile
}
func NewMailDir(name, basePath string) (*MailDir, error) {
mailDirPath := path.Join(basePath, name)
mailDir := &MailDir{basePath: basePath, name: name, MailFiles: []MailFile{}}
info, err := os.Stat(mailDirPath)
if err != nil {
if err == os.ErrNotExist {
err = os.Mkdir(mailDirPath, 0750)
}
}
if !info.IsDir() {
return mailDir, ErrPathIsNotDirectory
}
return mailDir, err
}