diff --git a/fs/maildir.go b/fs/maildir.go index a719e97..1fabaf6 100644 --- a/fs/maildir.go +++ b/fs/maildir.go @@ -2,15 +2,24 @@ 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)) @@ -19,13 +28,62 @@ func cat(f []Flag) string { } const ( - Seen Flag = "S" - Answered = "R" - Flagged = "F" - Deleted = "T" - Draft = "D" + 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,` @@ -79,6 +137,91 @@ func (m *MailFile) String() string { ) } -type MailDir struct { - basePath string +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 } diff --git a/fs/maildir_test.go b/fs/maildir_test.go index 1ceee36..e8388a2 100644 --- a/fs/maildir_test.go +++ b/fs/maildir_test.go @@ -3,10 +3,32 @@ package fs import ( "fmt" "testing" + "time" "github.com/google/go-cmp/cmp" ) +func TestFlagsFromString(t *testing.T) { + flags, err := flagsFromString("RTDSF") + if err != nil { + t.Errorf("Flag parsing errored unexpectedly: %s\n", err) + } + expect := []Flag{FlagDraft, FlagFlagged, FlagAnswered, FlagSeen, FlagDeleted} + if !cmp.Equal(flags, expect) { + t.Errorf("Flags mismatch: %s", cmp.Diff(expect, flags)) + } + + _, err = flagsFromString("RDX") + if err != ErrUnknownFlag { + t.Error("Flag parsing should have errored") + } + + _, err = flagsFromString("DDF") + if err != ErrMalformedName { + t.Error("Flag parsing should have errored") + } +} + func TestMailFileString(t *testing.T) { clock := mockClock{} timestamp := clock.Now() @@ -16,7 +38,7 @@ func TestMailFileString(t *testing.T) { name := "main_account" // known md5sum: f2cf513ad46d4d9b9684103e468803a0 uid := 33243 mf := &MailFile{timestamp: timestamp, progressive: progressive, pid: pid, hostname: hostname} - mf.SetFlags([]Flag{Seen, Answered}) + mf.SetFlags([]Flag{FlagSeen, FlagAnswered}) mf.SetMd5FromName(name) mf.SetUid(uid) @@ -34,3 +56,22 @@ func TestMailFileString(t *testing.T) { t.Logf("diff: %s", cmp.Diff(expect, result)) } } + +func TestNewMailFile(t *testing.T) { + mf, err := NewMailFile("12345_0.1312.myplace,U=666,FMD5=f2cf513ad46d4d9b9684103e468803a0:2,") + if err != nil { + t.Errorf("Unexpected error: %s\n", err) + } + expect := &MailFile{ + timestamp: time.Unix(12345, 0), + progressive: 0, + pid: 1312, + hostname: "myplace", + uid: 666, + md5: "f2cf513ad46d4d9b9684103e468803a0", + flags: []Flag{}, + } + if !cmp.Equal(mf, expect, cmp.AllowUnexported(MailFile{})) { + t.Errorf("MailFile mismatch: %s", cmp.Diff(expect, mf, cmp.AllowUnexported(MailFile{}))) + } +}