From 10bc07a18e5cba41c4bc3523c1bd5fda467f6fd7 Mon Sep 17 00:00:00 2001 From: James Hillyerd Date: Sun, 11 Mar 2018 22:25:21 -0700 Subject: [PATCH] message: Implement service layer, stubs for #81 I've made some effort to wire the manager into the controllers, but tests are currently failing. --- cmd/inbucket/main.go | 4 +- pkg/message/manager.go | 86 +++++++++++++++++++++++++++++++ pkg/message/message.go | 26 ++++++++++ pkg/rest/apiv1_controller.go | 60 +++++++++------------ pkg/rest/apiv1_controller_test.go | 75 ++++++++++++++++++++++++--- pkg/rest/model/apiv1_model.go | 3 +- pkg/rest/testutils_test.go | 5 +- pkg/server/web/context.go | 3 ++ pkg/server/web/server.go | 4 ++ pkg/storage/file/fmessage.go | 16 ------ pkg/storage/file/fstore_test.go | 2 - pkg/storage/storage.go | 1 - pkg/test/manager.go | 53 +++++++++++++++++++ pkg/webui/mailbox_controller.go | 64 ++++++++--------------- 14 files changed, 291 insertions(+), 111 deletions(-) create mode 100644 pkg/message/manager.go create mode 100644 pkg/message/message.go create mode 100644 pkg/test/manager.go diff --git a/cmd/inbucket/main.go b/cmd/inbucket/main.go index 9f1d116..ff01d3f 100644 --- a/cmd/inbucket/main.go +++ b/cmd/inbucket/main.go @@ -14,6 +14,7 @@ import ( "github.com/jhillyerd/inbucket/pkg/config" "github.com/jhillyerd/inbucket/pkg/log" + "github.com/jhillyerd/inbucket/pkg/message" "github.com/jhillyerd/inbucket/pkg/msghub" "github.com/jhillyerd/inbucket/pkg/rest" "github.com/jhillyerd/inbucket/pkg/server/pop3" @@ -123,7 +124,8 @@ func main() { retentionScanner.Start() // Start HTTP server - web.Initialize(config.GetWebConfig(), shutdownChan, ds, msgHub) + mm := &message.StoreManager{Store: ds} + web.Initialize(config.GetWebConfig(), shutdownChan, mm, ds, msgHub) webui.SetupRoutes(web.Router) rest.SetupRoutes(web.Router) go web.Start(rootCtx) diff --git a/pkg/message/manager.go b/pkg/message/manager.go new file mode 100644 index 0000000..5782273 --- /dev/null +++ b/pkg/message/manager.go @@ -0,0 +1,86 @@ +package message + +import ( + "io" + + "github.com/jhillyerd/enmime" + "github.com/jhillyerd/inbucket/pkg/storage" +) + +// Manager is the interface controllers use to interact with messages. +type Manager interface { + GetMetadata(mailbox string) ([]*Metadata, error) + GetMessage(mailbox, id string) (*Message, error) + PurgeMessages(mailbox string) error + RemoveMessage(mailbox, id string) error + SourceReader(mailbox, id string) (io.ReadCloser, error) +} + +// StoreManager is a message Manager backed by the storage.Store. +type StoreManager struct { + Store storage.Store +} + +// GetMetadata returns a slice of metadata for the specified mailbox. +func (s *StoreManager) GetMetadata(mailbox string) ([]*Metadata, error) { + messages, err := s.Store.GetMessages(mailbox) + if err != nil { + return nil, err + } + metas := make([]*Metadata, len(messages)) + for i, sm := range messages { + metas[i] = makeMetadata(sm) + } + return metas, nil +} + +// GetMessage returns the specified message. +func (s *StoreManager) GetMessage(mailbox, id string) (*Message, error) { + sm, err := s.Store.GetMessage(mailbox, id) + if err != nil { + return nil, err + } + r, err := sm.RawReader() + if err != nil { + return nil, err + } + env, err := enmime.ReadEnvelope(r) + if err != nil { + return nil, err + } + _ = r.Close() + header := makeMetadata(sm) + return &Message{Metadata: *header, Envelope: env}, nil +} + +// PurgeMessages removes all messages from the specified mailbox. +func (s *StoreManager) PurgeMessages(mailbox string) error { + return s.Store.PurgeMessages(mailbox) +} + +// RemoveMessage deletes the specified message. +func (s *StoreManager) RemoveMessage(mailbox, id string) error { + return s.Store.RemoveMessage(mailbox, id) +} + +// SourceReader allows the stored message source to be read. +func (s *StoreManager) SourceReader(mailbox, id string) (io.ReadCloser, error) { + sm, err := s.Store.GetMessage(mailbox, id) + if err != nil { + return nil, err + } + return sm.RawReader() +} + +// makeMetadata populates Metadata from a StoreMessage. +func makeMetadata(m storage.StoreMessage) *Metadata { + return &Metadata{ + Mailbox: m.Mailbox(), + ID: m.ID(), + From: m.From(), + To: m.To(), + Date: m.Date(), + Subject: m.Subject(), + Size: m.Size(), + } +} diff --git a/pkg/message/message.go b/pkg/message/message.go new file mode 100644 index 0000000..fd5f25b --- /dev/null +++ b/pkg/message/message.go @@ -0,0 +1,26 @@ +// Package message contains message handling logic. +package message + +import ( + "net/mail" + "time" + + "github.com/jhillyerd/enmime" +) + +// Metadata holds information about a message, but not the content. +type Metadata struct { + Mailbox string + ID string + From *mail.Address + To []*mail.Address + Date time.Time + Subject string + Size int64 +} + +// Message holds both the metadata and content of a message. +type Message struct { + Metadata + Envelope *enmime.Envelope +} diff --git a/pkg/rest/apiv1_controller.go b/pkg/rest/apiv1_controller.go index bc03bfd..135669e 100644 --- a/pkg/rest/apiv1_controller.go +++ b/pkg/rest/apiv1_controller.go @@ -24,7 +24,7 @@ func MailboxListV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) ( if err != nil { return err } - messages, err := ctx.DataStore.GetMessages(name) + messages, err := ctx.MsgSvc.GetMetadata(name) if err != nil { // This doesn't indicate empty, likely an IO error return fmt.Errorf("Failed to get messages for %v: %v", name, err) @@ -35,12 +35,12 @@ func MailboxListV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) ( for i, msg := range messages { jmessages[i] = &model.JSONMessageHeaderV1{ Mailbox: name, - ID: msg.ID(), - From: msg.From().String(), - To: stringutil.StringAddressList(msg.To()), - Subject: msg.Subject(), - Date: msg.Date(), - Size: msg.Size(), + ID: msg.ID, + From: msg.From.String(), + To: stringutil.StringAddressList(msg.To), + Subject: msg.Subject, + Date: msg.Date, + Size: msg.Size, } } return web.RenderJSON(w, jmessages) @@ -54,7 +54,7 @@ func MailboxShowV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) ( if err != nil { return err } - msg, err := ctx.DataStore.GetMessage(name, id) + msg, err := ctx.MsgSvc.GetMessage(name, id) if err == storage.ErrNotExist { http.NotFound(w, req) return nil @@ -63,14 +63,7 @@ func MailboxShowV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) ( // This doesn't indicate empty, likely an IO error return fmt.Errorf("GetMessage(%q) failed: %v", id, err) } - header, err := msg.ReadHeader() - if err != nil { - return fmt.Errorf("ReadHeader(%q) failed: %v", id, err) - } - mime, err := msg.ReadBody() - if err != nil { - return fmt.Errorf("ReadBody(%q) failed: %v", id, err) - } + mime := msg.Envelope attachments := make([]*model.JSONMessageAttachmentV1, len(mime.Attachments)) for i, att := range mime.Attachments { @@ -89,13 +82,13 @@ func MailboxShowV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) ( return web.RenderJSON(w, &model.JSONMessageV1{ Mailbox: name, - ID: msg.ID(), - From: msg.From().String(), - To: stringutil.StringAddressList(msg.To()), - Subject: msg.Subject(), - Date: msg.Date(), - Size: msg.Size(), - Header: header.Header, + ID: msg.ID, + From: msg.From.String(), + To: stringutil.StringAddressList(msg.To), + Subject: msg.Subject, + Date: msg.Date, + Size: msg.Size, + Header: mime.Root.Header, Body: &model.JSONMessageBodyV1{ Text: mime.Text, HTML: mime.HTML, @@ -112,7 +105,7 @@ func MailboxPurgeV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) return err } // Delete all messages - err = ctx.DataStore.PurgeMessages(name) + err = ctx.MsgSvc.PurgeMessages(name) if err != nil { return fmt.Errorf("Mailbox(%q) purge failed: %v", name, err) } @@ -129,25 +122,20 @@ func MailboxSourceV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) if err != nil { return err } - message, err := ctx.DataStore.GetMessage(name, id) + + r, err := ctx.MsgSvc.SourceReader(name, id) if err == storage.ErrNotExist { http.NotFound(w, req) return nil } if err != nil { // This doesn't indicate missing, likely an IO error - return fmt.Errorf("GetMessage(%q) failed: %v", id, err) + return fmt.Errorf("SourceReader(%q) failed: %v", id, err) } - raw, err := message.ReadRaw() - if err != nil { - return fmt.Errorf("ReadRaw(%q) failed: %v", id, err) - } - + // Output message source w.Header().Set("Content-Type", "text/plain") - if _, err := io.WriteString(w, *raw); err != nil { - return err - } - return nil + _, err = io.Copy(w, r) + return err } // MailboxDeleteV1 removes a particular message from a mailbox @@ -158,7 +146,7 @@ func MailboxDeleteV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) if err != nil { return err } - err = ctx.DataStore.RemoveMessage(name, id) + err = ctx.MsgSvc.RemoveMessage(name, id) if err == storage.ErrNotExist { http.NotFound(w, req) return nil diff --git a/pkg/rest/apiv1_controller_test.go b/pkg/rest/apiv1_controller_test.go index 8053a48..48d2841 100644 --- a/pkg/rest/apiv1_controller_test.go +++ b/pkg/rest/apiv1_controller_test.go @@ -4,10 +4,14 @@ import ( "encoding/json" "io" "net/mail" + "net/textproto" "os" + "strings" "testing" "time" + "github.com/jhillyerd/enmime" + "github.com/jhillyerd/inbucket/pkg/message" "github.com/jhillyerd/inbucket/pkg/test" ) @@ -31,7 +35,8 @@ const ( func TestRestMailboxList(t *testing.T) { // Setup ds := test.NewStore() - logbuf := setupWebServer(ds) + mm := test.NewManager() + logbuf := setupWebServer(mm, ds) // Test invalid mailbox name w, err := testRestGet(baseURL + "/mailbox/foo@bar") @@ -80,10 +85,24 @@ func TestRestMailboxList(t *testing.T) { Subject: "subject 2", Date: time.Date(2012, 7, 1, 10, 11, 12, 253, time.FixedZone("PDT", -700)), } - msg1 := data1.MockMessage() - msg2 := data2.MockMessage() - ds.AddMessage("good", msg1) - ds.AddMessage("good", msg2) + meta1 := message.Metadata{ + Mailbox: "good", + ID: "0001", + From: &mail.Address{Name: "", Address: "from1@host"}, + To: []*mail.Address{{Name: "", Address: "to1@host"}}, + Subject: "subject 1", + Date: time.Date(2012, 2, 1, 10, 11, 12, 253, time.FixedZone("PST", -800)), + } + meta2 := message.Metadata{ + Mailbox: "good", + ID: "0002", + From: &mail.Address{Name: "", Address: "from2@host"}, + To: []*mail.Address{{Name: "", Address: "to1@host"}}, + Subject: "subject 2", + Date: time.Date(2012, 7, 1, 10, 11, 12, 253, time.FixedZone("PDT", -700)), + } + mm.AddMessage("good", &message.Message{Metadata: meta1}) + mm.AddMessage("good", &message.Message{Metadata: meta2}) // Check return code w, err = testRestGet(baseURL + "/mailbox/good") @@ -96,6 +115,25 @@ func TestRestMailboxList(t *testing.T) { } // Check JSON + got := w.Body.String() + testStrings := []string{ + `{"mailbox":"good","id":"0001","from":"\u003cfrom1@host\u003e",` + + `"to":["\u003cto1@host\u003e"],"subject":"subject 1",` + + `"date":"2012-02-01T10:11:12.000000253-00:13","size":0}`, + `{"mailbox":"good","id":"0002","from":"\u003cfrom2@host\u003e",` + + `"to":["\u003cto1@host\u003e"],"subject":"subject 2",` + + `"date":"2012-07-01T10:11:12.000000253-00:11","size":0}`, + } + for _, ts := range testStrings { + t.Run(ts, func(t *testing.T) { + if !strings.Contains(got, ts) { + t.Errorf("got:\n%s\nwant to contain:\n%s", got, ts) + } + }) + } + + // Check JSON + // TODO transitional while refactoring dec := json.NewDecoder(w.Body) var result []interface{} if err := dec.Decode(&result); err != nil { @@ -128,7 +166,8 @@ func TestRestMailboxList(t *testing.T) { func TestRestMessage(t *testing.T) { // Setup ds := test.NewStore() - logbuf := setupWebServer(ds) + mm := test.NewManager() + logbuf := setupWebServer(mm, ds) // Test invalid mailbox name w, err := testRestGet(baseURL + "/mailbox/foo@bar/0001") @@ -168,6 +207,26 @@ func TestRestMessage(t *testing.T) { } // Test JSON message headers + msg1 := &message.Message{ + Metadata: message.Metadata{ + Mailbox: "good", + ID: "0001", + From: &mail.Address{Name: "", Address: "from1@host"}, + To: []*mail.Address{{Name: "", Address: "to1@host"}}, + Subject: "subject 1", + Date: time.Date(2012, 2, 1, 10, 11, 12, 253, time.FixedZone("PST", -800)), + }, + Envelope: &enmime.Envelope{ + Text: "This is some text", + HTML: "This is some HTML", + Root: &enmime.Part{ + Header: textproto.MIMEHeader{ + "To": []string{"fred@fish.com", "keyword@nsa.gov"}, + "From": []string{"noreply@inbucket.org"}, + }, + }, + }, + } data1 := &InputMessageData{ Mailbox: "good", ID: "0001", @@ -181,8 +240,7 @@ func TestRestMessage(t *testing.T) { Text: "This is some text", HTML: "This is some HTML", } - msg1 := data1.MockMessage() - ds.AddMessage("good", msg1) + mm.AddMessage("good", msg1) // Check return code w, err = testRestGet(baseURL + "/mailbox/good/0001") @@ -195,6 +253,7 @@ func TestRestMessage(t *testing.T) { } // Check JSON + // TODO transitional while refactoring dec := json.NewDecoder(w.Body) var result map[string]interface{} if err := dec.Decode(&result); err != nil { diff --git a/pkg/rest/model/apiv1_model.go b/pkg/rest/model/apiv1_model.go index 1e4f7f0..7e1e083 100644 --- a/pkg/rest/model/apiv1_model.go +++ b/pkg/rest/model/apiv1_model.go @@ -1,7 +1,6 @@ package model import ( - "net/mail" "time" ) @@ -26,7 +25,7 @@ type JSONMessageV1 struct { Date time.Time `json:"date"` Size int64 `json:"size"` Body *JSONMessageBodyV1 `json:"body"` - Header mail.Header `json:"header"` + Header map[string][]string `json:"header"` Attachments []*JSONMessageAttachmentV1 `json:"attachments"` } diff --git a/pkg/rest/testutils_test.go b/pkg/rest/testutils_test.go index 4b7d669..4bdcae5 100644 --- a/pkg/rest/testutils_test.go +++ b/pkg/rest/testutils_test.go @@ -11,6 +11,7 @@ import ( "github.com/jhillyerd/enmime" "github.com/jhillyerd/inbucket/pkg/config" + "github.com/jhillyerd/inbucket/pkg/message" "github.com/jhillyerd/inbucket/pkg/msghub" "github.com/jhillyerd/inbucket/pkg/server/web" "github.com/jhillyerd/inbucket/pkg/storage" @@ -193,7 +194,7 @@ func testRestGet(url string) (*httptest.ResponseRecorder, error) { return w, nil } -func setupWebServer(ds storage.Store) *bytes.Buffer { +func setupWebServer(mm message.Manager, ds storage.Store) *bytes.Buffer { // Capture log output buf := new(bytes.Buffer) log.SetOutput(buf) @@ -205,7 +206,7 @@ func setupWebServer(ds storage.Store) *bytes.Buffer { PublicDir: "../themes/bootstrap/public", } shutdownChan := make(chan bool) - web.Initialize(cfg, shutdownChan, ds, &msghub.Hub{}) + web.Initialize(cfg, shutdownChan, mm, ds, &msghub.Hub{}) SetupRoutes(web.Router) return buf diff --git a/pkg/server/web/context.go b/pkg/server/web/context.go index 422fdb5..af5f6b2 100644 --- a/pkg/server/web/context.go +++ b/pkg/server/web/context.go @@ -7,6 +7,7 @@ import ( "github.com/gorilla/mux" "github.com/gorilla/sessions" "github.com/jhillyerd/inbucket/pkg/config" + "github.com/jhillyerd/inbucket/pkg/message" "github.com/jhillyerd/inbucket/pkg/msghub" "github.com/jhillyerd/inbucket/pkg/storage" ) @@ -17,6 +18,7 @@ type Context struct { Session *sessions.Session DataStore storage.Store MsgHub *msghub.Hub + MsgSvc message.Manager WebConfig config.WebConfig IsJSON bool } @@ -61,6 +63,7 @@ func NewContext(req *http.Request) (*Context, error) { Session: sess, DataStore: DataStore, MsgHub: msgHub, + MsgSvc: msgSvc, WebConfig: webConfig, IsJSON: headerMatch(req, "Accept", "application/json"), } diff --git a/pkg/server/web/server.go b/pkg/server/web/server.go index 5c82430..c76a2f7 100644 --- a/pkg/server/web/server.go +++ b/pkg/server/web/server.go @@ -14,6 +14,7 @@ import ( "github.com/gorilla/sessions" "github.com/jhillyerd/inbucket/pkg/config" "github.com/jhillyerd/inbucket/pkg/log" + "github.com/jhillyerd/inbucket/pkg/message" "github.com/jhillyerd/inbucket/pkg/msghub" "github.com/jhillyerd/inbucket/pkg/storage" ) @@ -27,6 +28,7 @@ var ( // msgHub holds a reference to the message pub/sub system msgHub *msghub.Hub + msgSvc message.Manager // Router is shared between httpd, webui and rest packages. It sends // incoming requests to the correct handler function @@ -51,6 +53,7 @@ func init() { func Initialize( cfg config.WebConfig, shutdownChan chan bool, + mm message.Manager, ds storage.Store, mh *msghub.Hub) { @@ -60,6 +63,7 @@ func Initialize( // NewContext() will use this DataStore for the web handlers DataStore = ds msgHub = mh + msgSvc = mm // Content Paths log.Infof("HTTP templates mapped to %q", cfg.TemplateDir) diff --git a/pkg/storage/file/fmessage.go b/pkg/storage/file/fmessage.go index 3f8b0d7..e9df65d 100644 --- a/pkg/storage/file/fmessage.go +++ b/pkg/storage/file/fmessage.go @@ -99,22 +99,6 @@ func (m *Message) rawPath() string { return filepath.Join(m.mailbox.path, m.Fid+".raw") } -// ReadHeader opens the .raw portion of a Message and returns a standard Go mail.Message object -func (m *Message) ReadHeader() (msg *mail.Message, err error) { - file, err := os.Open(m.rawPath()) - if err != nil { - return nil, err - } - defer func() { - if err := file.Close(); err != nil { - log.Errorf("Failed to close %q: %v", m.rawPath(), err) - } - }() - - reader := bufio.NewReader(file) - return mail.ReadMessage(reader) -} - // ReadBody opens the .raw portion of a Message and returns a MIMEBody object func (m *Message) ReadBody() (body *enmime.Envelope, err error) { file, err := os.Open(m.rawPath()) diff --git a/pkg/storage/file/fstore_test.go b/pkg/storage/file/fstore_test.go index 8db7253..eb5add1 100644 --- a/pkg/storage/file/fstore_test.go +++ b/pkg/storage/file/fstore_test.go @@ -337,8 +337,6 @@ func TestFSMissing(t *testing.T) { assert.Nil(t, err) // Try to read parts of message - _, err = msg.ReadHeader() - assert.Error(t, err) _, err = msg.ReadBody() assert.Error(t, err) diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index c3d2019..52e50cc 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -41,7 +41,6 @@ type StoreMessage interface { Date() time.Time Subject() string RawReader() (reader io.ReadCloser, err error) - ReadHeader() (msg *mail.Message, err error) ReadBody() (body *enmime.Envelope, err error) ReadRaw() (raw *string, err error) Append(data []byte) error diff --git a/pkg/test/manager.go b/pkg/test/manager.go new file mode 100644 index 0000000..47c87a8 --- /dev/null +++ b/pkg/test/manager.go @@ -0,0 +1,53 @@ +package test + +import ( + "errors" + + "github.com/jhillyerd/inbucket/pkg/message" + "github.com/jhillyerd/inbucket/pkg/storage" +) + +// ManagerStub is a test stub for message.Manager +type ManagerStub struct { + message.Manager + mailboxes map[string][]*message.Message +} + +// NewManager creates a new ManagerStub. +func NewManager() *ManagerStub { + return &ManagerStub{ + mailboxes: make(map[string][]*message.Message), + } +} + +// AddMessage adds a message to the specified mailbox. +func (m *ManagerStub) AddMessage(mailbox string, msg *message.Message) { + messages := m.mailboxes[mailbox] + m.mailboxes[mailbox] = append(messages, msg) +} + +// GetMessage gets a message by ID from the specified mailbox. +func (m *ManagerStub) GetMessage(mailbox, id string) (*message.Message, error) { + if mailbox == "messageerr" { + return nil, errors.New("internal error") + } + for _, msg := range m.mailboxes[mailbox] { + if msg.ID == id { + return msg, nil + } + } + return nil, storage.ErrNotExist +} + +// GetMetadata gets all the metadata for the specified mailbox. +func (m *ManagerStub) GetMetadata(mailbox string) ([]*message.Metadata, error) { + if mailbox == "messageserr" { + return nil, errors.New("internal error") + } + messages := m.mailboxes[mailbox] + metas := make([]*message.Metadata, len(messages)) + for i, msg := range messages { + metas[i] = &msg.Metadata + } + return metas, nil +} diff --git a/pkg/webui/mailbox_controller.go b/pkg/webui/mailbox_controller.go index 69b3a3f..ed092e7 100644 --- a/pkg/webui/mailbox_controller.go +++ b/pkg/webui/mailbox_controller.go @@ -72,7 +72,7 @@ func MailboxList(w http.ResponseWriter, req *http.Request, ctx *web.Context) (er if err != nil { return err } - messages, err := ctx.DataStore.GetMessages(name) + messages, err := ctx.MsgSvc.GetMetadata(name) if err != nil { // This doesn't indicate empty, likely an IO error return fmt.Errorf("Failed to get messages for %v: %v", name, err) @@ -94,7 +94,7 @@ func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *web.Context) (er if err != nil { return err } - msg, err := ctx.DataStore.GetMessage(name, id) + msg, err := ctx.MsgSvc.GetMessage(name, id) if err == storage.ErrNotExist { http.NotFound(w, req) return nil @@ -103,10 +103,7 @@ func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *web.Context) (er // This doesn't indicate empty, likely an IO error return fmt.Errorf("GetMessage(%q) failed: %v", id, err) } - mime, err := msg.ReadBody() - if err != nil { - return fmt.Errorf("ReadBody(%q) failed: %v", id, err) - } + mime := msg.Envelope body := template.HTML(web.TextToHTML(mime.Text)) htmlAvailable := mime.HTML != "" var htmlBody template.HTML @@ -138,25 +135,22 @@ func MailboxHTML(w http.ResponseWriter, req *http.Request, ctx *web.Context) (er if err != nil { return err } - message, err := ctx.DataStore.GetMessage(name, id) + msg, err := ctx.MsgSvc.GetMessage(name, id) if err == storage.ErrNotExist { http.NotFound(w, req) return nil } if err != nil { - // This doesn't indicate missing, likely an IO error + // This doesn't indicate empty, likely an IO error return fmt.Errorf("GetMessage(%q) failed: %v", id, err) } - mime, err := message.ReadBody() - if err != nil { - return fmt.Errorf("ReadBody(%q) failed: %v", id, err) - } + mime := msg.Envelope // Render partial template w.Header().Set("Content-Type", "text/html; charset=UTF-8") return web.RenderPartial("mailbox/_html.html", w, map[string]interface{}{ "ctx": ctx, "name": name, - "message": message, + "message": msg, "body": template.HTML(mime.HTML), }) } @@ -169,25 +163,19 @@ func MailboxSource(w http.ResponseWriter, req *http.Request, ctx *web.Context) ( if err != nil { return err } - message, err := ctx.DataStore.GetMessage(name, id) + r, err := ctx.MsgSvc.SourceReader(name, id) if err == storage.ErrNotExist { http.NotFound(w, req) return nil } if err != nil { // This doesn't indicate missing, likely an IO error - return fmt.Errorf("GetMessage(%q) failed: %v", id, err) - } - raw, err := message.ReadRaw() - if err != nil { - return fmt.Errorf("ReadRaw(%q) failed: %v", id, err) + return fmt.Errorf("SourceReader(%q) failed: %v", id, err) } // Output message source w.Header().Set("Content-Type", "text/plain") - if _, err := io.WriteString(w, *raw); err != nil { - return err - } - return nil + _, err = io.Copy(w, r) + return err } // MailboxDownloadAttach sends the attachment to the client; disposition: @@ -210,19 +198,16 @@ func MailboxDownloadAttach(w http.ResponseWriter, req *http.Request, ctx *web.Co http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther) return nil } - message, err := ctx.DataStore.GetMessage(name, id) + msg, err := ctx.MsgSvc.GetMessage(name, id) if err == storage.ErrNotExist { http.NotFound(w, req) return nil } if err != nil { - // This doesn't indicate missing, likely an IO error + // This doesn't indicate empty, likely an IO error return fmt.Errorf("GetMessage(%q) failed: %v", id, err) } - body, err := message.ReadBody() - if err != nil { - return err - } + body := msg.Envelope if int(num) >= len(body.Attachments) { ctx.Session.AddFlash("Attachment number too high", "errors") _ = ctx.Session.Save(req, w) @@ -233,10 +218,8 @@ func MailboxDownloadAttach(w http.ResponseWriter, req *http.Request, ctx *web.Co // Output attachment w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Disposition", "attachment") - if _, err := io.Copy(w, part); err != nil { - return err - } - return nil + _, err = io.Copy(w, part) + return err } // MailboxViewAttach sends the attachment to the client for online viewing @@ -258,19 +241,16 @@ func MailboxViewAttach(w http.ResponseWriter, req *http.Request, ctx *web.Contex http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther) return nil } - message, err := ctx.DataStore.GetMessage(name, id) + msg, err := ctx.MsgSvc.GetMessage(name, id) if err == storage.ErrNotExist { http.NotFound(w, req) return nil } if err != nil { - // This doesn't indicate missing, likely an IO error + // This doesn't indicate empty, likely an IO error return fmt.Errorf("GetMessage(%q) failed: %v", id, err) } - body, err := message.ReadBody() - if err != nil { - return err - } + body := msg.Envelope if int(num) >= len(body.Attachments) { ctx.Session.AddFlash("Attachment number too high", "errors") _ = ctx.Session.Save(req, w) @@ -280,8 +260,6 @@ func MailboxViewAttach(w http.ResponseWriter, req *http.Request, ctx *web.Contex part := body.Attachments[num] // Output attachment w.Header().Set("Content-Type", part.ContentType) - if _, err := io.Copy(w, part); err != nil { - return err - } - return nil + _, err = io.Copy(w, part) + return err }