From 6e8e0a300c073a376a1e56d5147243c4cde3fd26 Mon Sep 17 00:00:00 2001 From: James Hillyerd Date: Sun, 7 Oct 2012 22:05:07 -0700 Subject: [PATCH] Server now writes raw smtp data to datastore. Does not yet create gob files. --- app/inbucket/datastore.go | 113 ++++++++++++++++++++++++++++++++++++++ app/smtpd/handler.go | 26 ++++++++- app/smtpd/listener.go | 8 ++- conf/app.conf | 2 + 4 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 app/inbucket/datastore.go diff --git a/app/inbucket/datastore.go b/app/inbucket/datastore.go new file mode 100644 index 0000000..c8eaf56 --- /dev/null +++ b/app/inbucket/datastore.go @@ -0,0 +1,113 @@ +package inbucket + +import ( + "bufio" + "fmt" + "github.com/robfig/revel" + "os" + "path/filepath" + "errors" + "time" +) + +var ErrNotWritable = errors.New("MailObject not writable") + +// Global because we only want one regardless of the number of DataStore objects +var countChannel = make(chan int, 10) + +func init() { + // Start generator + go countGenerator(countChannel) +} + +// Populates the channel with numbers +func countGenerator(c chan int) { + for i := 0; true; i = (i + 1) % 10000 { + c <- i + } +} + +type DataStore struct { + path string + mailPath string +} + +func NewDataStore() *DataStore { + path, found := rev.Config.String("datastore.path") + if found { + mailPath := filepath.Join(path, "mail") + return &DataStore{path: path, mailPath: mailPath} + } + rev.ERROR.Printf("No value configured for datastore.path") + return nil +} + +type MailObject struct { + store *DataStore + mailbox string + rawPath string + gobPath string + writable bool + writerFile *os.File + writer *bufio.Writer +} + +func (ds *DataStore) NewMailObject(emailAddress string) *MailObject { + mailbox := ParseMailboxName(emailAddress) + maildir := HashMailboxName(mailbox) + fileBase := time.Now().Format("20060102T150405") + "-" + fmt.Sprintf("%04d", <-countChannel) + boxPath := filepath.Join(ds.mailPath, maildir) + if err := os.MkdirAll(boxPath, 0770); err != nil { + rev.ERROR.Printf("Failed to create directory %v, %v", boxPath, err) + return nil + } + pathBase := filepath.Join(boxPath, fileBase) + + return &MailObject{store: ds, mailbox: mailbox, rawPath: pathBase + ".raw", + gobPath: pathBase + ".gob", writable: true} +} + +func (m *MailObject) Mailbox() string { + return m.mailbox +} + +func (m *MailObject) Append(data []byte) error { + // Prevent Appending to a pre-existing MailObject + if !m.writable { + return ErrNotWritable + } + // Open file for writing if we haven't yet + if m.writer == nil { + file, err := os.Create(m.rawPath) + if err != nil { + // Set writable false just in case something calls me a million times + m.writable = false + return err + } + m.writerFile = file + m.writer = bufio.NewWriter(file) + } + _, err := m.writer.Write(data) + return err +} + +func (m *MailObject) Close() error { + // nil out the fields so they can't be used + writer := m.writer + writerFile := m.writerFile + m.writer = nil + m.writerFile = nil + + if (writer != nil) { + if err := writer.Flush(); err != nil { + return err + } + } + if (writerFile != nil) { + if err := writerFile.Close(); err != nil { + return err + } + } + + return nil +} diff --git a/app/smtpd/handler.go b/app/smtpd/handler.go index f8fedbe..1bbab30 100644 --- a/app/smtpd/handler.go +++ b/app/smtpd/handler.go @@ -7,6 +7,7 @@ import ( "net" "strings" "time" + "github.com/jhillyerd/inbucket/app/inbucket" ) type State int @@ -235,7 +236,16 @@ func (ss *Session) mailHandler(cmd string, arg string) { // DATA func (ss *Session) dataHandler() { - var msgSize uint64 = 0 + msgSize := uint64(0) + + // Get a MailObject for each recipient + mailObjects := make([]*inbucket.MailObject, ss.recipients.Len()) + i := 0 + for e := ss.recipients.Front(); e != nil; e = e.Next() { + mailObjects[i] = ss.server.dataStore.NewMailObject(e.Value.(string)) + i++ + } + ss.send("354 Start mail input; end with .") for { line, err := ss.readLine() @@ -251,6 +261,9 @@ func (ss *Session) dataHandler() { } if line == ".\r\n" || line == ".\n" { // Mail data complete + for _, mo := range mailObjects { + mo.Close() + } ss.send("250 Mail accepted for delivery") ss.info("Message size %v bytes", msgSize) ss.enterState(READY) @@ -260,7 +273,16 @@ func (ss *Session) dataHandler() { line = line[1:] } msgSize += uint64(len(line)) - // TODO: Add variable line to something! + // Append to message objects + for _, mo := range mailObjects { + if err := mo.Append([]byte(line)); err != nil { + ss.error("Failed to append to mailbox %v: %v", mo.Mailbox(), err) + ss.send("554 Something went wrong") + ss.enterState(READY) + // TODO: Should really cleanup the crap on filesystem... + return + } + } } } diff --git a/app/smtpd/listener.go b/app/smtpd/listener.go index 9c309ba..943b481 100644 --- a/app/smtpd/listener.go +++ b/app/smtpd/listener.go @@ -2,8 +2,9 @@ package smtpd import ( "fmt" - "net" + "github.com/jhillyerd/inbucket/app/inbucket" "github.com/robfig/revel" + "net" ) // Real server code starts here @@ -12,11 +13,14 @@ type Server struct { port int maxRecips int maxIdleSeconds int + dataStore *inbucket.DataStore } // Init a new Server object func New(domain string, port int) *Server { - return &Server{domain: domain, port: port, maxRecips: 100, maxIdleSeconds: 60} + ds := inbucket.NewDataStore() + return &Server{domain: domain, port: port, maxRecips: 100, maxIdleSeconds: 60, + dataStore: ds} } // Loggers diff --git a/conf/app.conf b/conf/app.conf index 2b45c7f..8e24a2c 100644 --- a/conf/app.conf +++ b/conf/app.conf @@ -8,6 +8,7 @@ results.pretty=true server.watcher=true smtpd.domain=skynet smtpd.port=2500 +datastore.path=/tmp/inbucket log.trace.output = off log.info.output = stderr @@ -19,6 +20,7 @@ results.pretty=false server.watcher=false smtpd.domain=skynet smtpd.port=2500 +datastore.path=/tmp/inbucket log.trace.output = off log.info.output = off