227 line
4.8 KiB
Go
227 line
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
|
|
}
|