diff options
| author | Shulhan <ms@kilabit.info> | 2019-03-01 09:39:31 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2019-03-01 10:26:33 +0700 |
| commit | db96ed72b1dbebaaa856c0a2d627df33da6827f2 (patch) | |
| tree | 05a37a55622b5f593bfb88402ed2ce303861cae7 | |
| parent | 85a7b727af871676c47373caff4806ac898e697d (diff) | |
| download | pakakeh.go-db96ed72b1dbebaaa856c0a2d627df33da6827f2.tar.xz | |
email/maildir: a library to manage email using maildir format
Currently it support the following functions,
* OutQueue(): creating temporary email before sending,
* DeleteOutQueue(): deleting sent email,
* Delete(): deleting email in "cur" directory,
* Incoming(): saving incoming email in "new" directory, and
* Get(): moving emails from "new" to "cur".
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | lib/email/maildir/manager.go | 283 | ||||
| -rw-r--r-- | lib/email/maildir/manager_test.go | 153 |
3 files changed, 437 insertions, 0 deletions
@@ -5,4 +5,5 @@ /lib/dns/testdata/hosts.block.out /lib/git/testdata/beku_test cover.* +lib/email/maildir/testdata/ lib/memfs/testdata/memfs_generate.go diff --git a/lib/email/maildir/manager.go b/lib/email/maildir/manager.go new file mode 100644 index 00000000..aa96cff7 --- /dev/null +++ b/lib/email/maildir/manager.go @@ -0,0 +1,283 @@ +// Copyright 2019, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +// Package maildir provide a library to manage email using maildir format. +// +// References +// +// [1] http://www.qmail.org/qmail-manual-html/man5/maildir.html +// +// [2] https://cr.yp.to/proto/maildir.html +// +package maildir + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "time" + + libtime "github.com/shuLhan/share/lib/time" +) + +// +// Manager manage email in a directory. +// +type Manager struct { + dirCur string + dirNew string + dirOut string + dirTmp string + hostname string + pid int + counter int +} + +// +// New create new maildir Manager in directory and initialize the hostname, +// pid, and counter for generating unique name. +// +func New(dir string) (mg *Manager, err error) { + if len(dir) == 0 { + return nil, fmt.Errorf("email/maildir: New: empty base directory") + } + + mg = &Manager{ + pid: os.Getpid(), + } + + err = mg.initDirs(dir) + if err != nil { + return nil, err + } + + mg.hostname, err = os.Hostname() + if len(mg.hostname) == 0 && err != nil { + mg.hostname = os.Getenv("HOST") + if len(mg.hostname) == 0 { + mg.hostname = "localhost" + } + } + + return mg, nil +} + +func (mg *Manager) initDirs(dir string) (err error) { + mg.dirCur = filepath.Join(dir, "cur") + err = os.MkdirAll(mg.dirCur, 0750) + if err != nil { + return fmt.Errorf("email/maildir: initDirs: %s", err.Error()) + } + + mg.dirNew = filepath.Join(dir, "new") + err = os.MkdirAll(mg.dirNew, 0750) + if err != nil { + return fmt.Errorf("email/maildir: initDirs: %s", err.Error()) + } + + mg.dirOut = filepath.Join(dir, "out") + err = os.MkdirAll(mg.dirOut, 0700) + if err != nil { + return fmt.Errorf("email/maildir: initDirs: %s", err.Error()) + } + + mg.dirTmp = filepath.Join(dir, "tmp") + err = os.MkdirAll(mg.dirTmp, 0700) + if err != nil { + return fmt.Errorf("email/maildir: initDirs: %s", err.Error()) + } + + return nil +} + +// +// Delete email file in "cur". +// +func (mg *Manager) Delete(fname string) (err error) { + if len(fname) == 0 { + return fmt.Errorf("email/maildir: Delete: empty file name") + } + + fdel := filepath.Join(mg.dirCur, fname) + + err = os.Remove(fdel) + if err != nil { + return fmt.Errorf("email/maildir: Delete: %s", err.Error()) + } + + return nil +} + +// +// DeleteOutQueue delete temporary file in send queue. +// +func (mg *Manager) DeleteOutQueue(fname string) (err error) { + if len(fname) == 0 { + return nil + } + + fname = filepath.Join(mg.dirOut, fname) + + err = os.Remove(fname) + if err != nil { + return fmt.Errorf("email/maildir: DeleteOutQueue: %s", err.Error()) + } + + return nil +} + +// +// OutQueue save the email in temporary queue directory before sending it to +// external MTA or processed. +// +// When mail is coming from MUA and received by server, the mail need +// to be successfully stored into disk by server, before replying with +// "250 OK" to client. +// +func (mg *Manager) OutQueue(email []byte) (err error) { + if len(email) == 0 { + return nil + } + + fname, _, err := mg.generateUniqueName(mg.dirOut) + if err != nil { + return err + } + + err = ioutil.WriteFile(fname, email, 0400) + if err != nil { + err = fmt.Errorf("email/maildir: OutQueue: %s", err.Error()) + return err + } + + mg.counter++ + + return nil +} + +// +// Get will move email from "new" to "cur". +// +func (mg *Manager) Get(fname string) (err error) { + if len(fname) == 0 { + return nil + } + + src := filepath.Join(mg.dirNew, fname) + dst := filepath.Join(mg.dirCur, fname) + + err = os.Rename(src, dst) + if err != nil { + return fmt.Errorf("email/maildir: Read: %s", err.Error()) + } + + return nil +} + +// +// Incoming save incoming message, from external MTA, in directory +// "${dir}/tmp/${unique}". Upon success, hard link it to +// "${dir}/new/${unique}" and delete the temporary file. +// +func (mg *Manager) Incoming(email []byte) (err error) { + if len(email) == 0 { + return nil + } + + tmpFile, uniqueName, err := mg.generateUniqueName(mg.dirTmp) + if err != nil { + return err + } + + err = ioutil.WriteFile(tmpFile, email, 0660) + if err != nil { + err = fmt.Errorf("email/maildir: Incoming: %s", err.Error()) + return err + } + + newFile := filepath.Join(mg.dirNew, uniqueName) + + err = os.Link(tmpFile, newFile) + if err != nil { + _ = os.Remove(tmpFile) + err = fmt.Errorf("email/maildir: Incoming: %s", err.Error()) + return err + } + + err = os.Remove(tmpFile) + if err != nil { + log.Printf("email/maildir: Incoming: %s", err.Error()) + } + + mg.counter++ + + return nil +} + +// +// RemoveAll remove all files inside a directory. +// +func (mg *Manager) RemoveAll(dir string) { + d, err := os.Open(dir) + if err != nil { + log.Println("email/maildir: RemoveAll: " + err.Error()) + return + } + fis, err := d.Readdir(0) + if err != nil { + log.Println("email/maildir: RemoveAll: " + err.Error()) + return + } + for _, fi := range fis { + if fi.IsDir() { + continue + } + + file := filepath.Join(dir, fi.Name()) + err = os.Remove(file) + if err != nil { + log.Println("email/maildir: RemoveAll: " + err.Error()) + } + } +} + +// +// generateUniqueName try generate unique name until 5 attempts or return an +// error. +// +func (mg *Manager) generateUniqueName(dir string) (fname, uniqueName string, err error) { + x := 0 + for x < 5 { + uniqueName = mg.uniqueName() + fname = filepath.Join(dir, uniqueName) + _, err = os.Stat(fname) + if err != nil { + if os.IsNotExist(err) { + return fname, uniqueName, nil + } + } + time.Sleep(2 * time.Second) + x++ + } + + err = fmt.Errorf("email/maildir: OutQueue: %s", err.Error()) + + return "", "", err +} + +// +// uniqueName generate a unique name using the following format, +// +// UnixTimestamp "." "M"(microsecond) "P"(ProcessID) "Q"(Counter) "." +// hostname +// +func (mg *Manager) uniqueName() string { + now := time.Now() + + return fmt.Sprintf("%d.M%dP%dQ%d.%s", now.Unix(), + libtime.Microsecond(&now), mg.pid, mg.counter, mg.hostname) +} diff --git a/lib/email/maildir/manager_test.go b/lib/email/maildir/manager_test.go new file mode 100644 index 00000000..c4b2a2d9 --- /dev/null +++ b/lib/email/maildir/manager_test.go @@ -0,0 +1,153 @@ +// Copyright 2019, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package maildir + +import ( + "log" + "os" + "testing" + + "github.com/shuLhan/share/lib/test" +) + +const testDir = "testdata" + +func lsDir(dir string) (ls []os.FileInfo) { + d, err := os.Open(dir) + if err != nil { + if os.IsNotExist(err) { + return + } + log.Fatal(err) + } + + ls, err = d.Readdir(0) + if err != nil { + log.Fatal(err) + } + + _ = d.Close() + + return ls +} + +func TestOutQueue(t *testing.T) { + mg, err := New(testDir) + if err != nil { + t.Fatal(err) + } + + mg.RemoveAll(mg.dirOut) + + cases := []struct { + desc string + email []byte + expNList int + expErr string + }{{ + desc: "With empty email", + expErr: "email/maildir: OutQueue: empty email", + }, { + desc: "With valid inputs", + email: []byte("From: me@localhost"), + expNList: 1, + }} + + for _, c := range cases { + t.Log(c.desc) + + err = mg.OutQueue(c.email) + if err != nil { + test.Assert(t, "error", c.expErr, err.Error(), true) + continue + } + + ls := lsDir(mg.dirOut) + + test.Assert(t, "n List", c.expNList, len(ls), true) + } +} + +func TestDeleteOutQueue(t *testing.T) { + mg, err := New(testDir) + if err != nil { + t.Fatal(err) + } + + listOut := lsDir(mg.dirOut) + + test.Assert(t, "n List", 1, len(listOut), true) + + cases := []struct { + desc string + fname string + expErr string + expNList int + }{{ + desc: "With empty filename", + expErr: "email/maildir: DeleteOutQueue: empty file name", + expNList: 1, + }, { + desc: "With valid filename", + fname: listOut[0].Name(), + expNList: 0, + }} + + for _, c := range cases { + t.Log(c.desc) + + err := mg.DeleteOutQueue(c.fname) + if err != nil { + test.Assert(t, "error", c.expErr, err.Error(), true) + continue + } + + ls := lsDir(mg.dirOut) + + test.Assert(t, "n List", c.expNList, len(ls), true) + } +} + +func TestIncoming(t *testing.T) { + mg, err := New(testDir) + if err != nil { + t.Fatal(err) + } + + mg.RemoveAll(mg.dirNew) + mg.RemoveAll(mg.dirTmp) + + cases := []struct { + desc string + email []byte + expErr string + expNTmp int + expNNew int + }{{ + desc: "With empty email", + expErr: "email/maildir: Incoming: empty email", + }, { + desc: "With valid parameters", + email: []byte("From: me@localhost"), + expNTmp: 0, + expNNew: 1, + }} + + for _, c := range cases { + t.Log(c.desc) + + err := mg.Incoming(c.email) + if err != nil { + test.Assert(t, "error", c.expErr, err.Error(), true) + continue + } + + lsTmp := lsDir(mg.dirTmp) + lsNew := lsDir(mg.dirNew) + + test.Assert(t, "n list tmp", c.expNTmp, len(lsTmp), true) + test.Assert(t, "n list new", c.expNNew, len(lsNew), true) + } +} |
