From cc5cd7f9c322e353588f4b761a9abfce44d6f0c3 Mon Sep 17 00:00:00 2001 From: James Hillyerd Date: Sat, 31 Mar 2018 21:46:10 -0700 Subject: [PATCH] storage: Add Seen flag, tests for #58 --- CHANGELOG.md | 4 ++++ pkg/message/message.go | 6 ++++++ pkg/storage/file/fmessage.go | 6 ++++++ pkg/storage/file/fstore.go | 26 +++++++++++++++++++++++ pkg/storage/mem/message.go | 4 ++++ pkg/storage/mem/store.go | 11 ++++++++++ pkg/storage/storage.go | 2 ++ pkg/test/storage_suite.go | 41 ++++++++++++++++++++++++++++++++++++ 8 files changed, 100 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1872bd..127330b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,10 +16,14 @@ This project adheres to [Semantic Versioning](http://semver.org/). page. - Debian `.deb` package generation to release process. - RedHat `.rpm` package generation to release process. +- Message seen flag in REST and Web UI so you can see which messages have + already been read. ### Changed - Massive refactor of back-end code. Inbucket should now be both easier and more enjoyable to work on. +- Changes to file storage format, will require pre-2.0 mail store directories to + be deleted. - Renamed `themes` directory to `ui` and eliminated the intermediate `bootstrap` directory. - Docker build: diff --git a/pkg/message/message.go b/pkg/message/message.go index 8bdd460..8c97cda 100644 --- a/pkg/message/message.go +++ b/pkg/message/message.go @@ -21,6 +21,7 @@ type Metadata struct { Date time.Time Subject string Size int64 + Seen bool } // Message holds both the metadata and content of a message. @@ -109,3 +110,8 @@ func (d *Delivery) Size() int64 { func (d *Delivery) Source() (io.ReadCloser, error) { return ioutil.NopCloser(d.Reader), nil } + +// Seen getter. +func (d *Delivery) Seen() bool { + return d.Meta.Seen +} diff --git a/pkg/storage/file/fmessage.go b/pkg/storage/file/fmessage.go index 961df62..a7ed1e2 100644 --- a/pkg/storage/file/fmessage.go +++ b/pkg/storage/file/fmessage.go @@ -21,6 +21,7 @@ type Message struct { Fto []*mail.Address Fsubject string Fsize int64 + Fseen bool } // newMessage creates a new FileMessage object and sets the Date and ID fields. @@ -96,3 +97,8 @@ func (m *Message) Source() (reader io.ReadCloser, err error) { } return file, nil } + +// Seen returns the seen flag value. +func (m *Message) Seen() bool { + return m.Fseen +} diff --git a/pkg/storage/file/fstore.go b/pkg/storage/file/fstore.go index 75ec3ba..b31ea0f 100644 --- a/pkg/storage/file/fstore.go +++ b/pkg/storage/file/fstore.go @@ -147,6 +147,32 @@ func (fs *Store) GetMessages(mailbox string) ([]storage.Message, error) { return mb.getMessages() } +// MarkSeen flags the message as having been read. +func (fs *Store) MarkSeen(mailbox, id string) error { + mb, err := fs.mbox(mailbox) + if err != nil { + return err + } + mb.Lock() + defer mb.Unlock() + if !mb.indexLoaded { + if err := mb.readIndex(); err != nil { + return err + } + } + for _, m := range mb.messages { + if m.Fid == id { + if m.Fseen { + // Already marked seen. + return nil + } + m.Fseen = true + break + } + } + return mb.writeIndex() +} + // RemoveMessage deletes a message by ID from the specified mailbox. func (fs *Store) RemoveMessage(mailbox, id string) error { mb, err := fs.mbox(mailbox) diff --git a/pkg/storage/mem/message.go b/pkg/storage/mem/message.go index b5ca498..02ae503 100644 --- a/pkg/storage/mem/message.go +++ b/pkg/storage/mem/message.go @@ -21,6 +21,7 @@ type Message struct { date time.Time subject string source []byte + seen bool el *list.Element // This message in Store.messages } @@ -51,3 +52,6 @@ func (m *Message) Source() (io.ReadCloser, error) { // Size returns the message size in bytes. func (m *Message) Size() int64 { return int64(len(m.source)) } + +// Seen returns the message seen flag. +func (m *Message) Seen() bool { return m.seen } diff --git a/pkg/storage/mem/store.go b/pkg/storage/mem/store.go index c37b241..16b094e 100644 --- a/pkg/storage/mem/store.go +++ b/pkg/storage/mem/store.go @@ -112,6 +112,17 @@ func (s *Store) GetMessages(mailbox string) (ms []storage.Message, err error) { return ms, err } +// MarkSeen marks a message as having been read. +func (s *Store) MarkSeen(mailbox, id string) error { + s.withMailbox(mailbox, true, func(mb *mbox) { + m := mb.messages[id] + if m != nil { + m.seen = true + } + }) + return nil +} + // PurgeMessages deletes the contents of a mailbox. func (s *Store) PurgeMessages(mailbox string) error { var messages map[string]*Message diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 67ef9fc..7cd40b3 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -28,6 +28,7 @@ type Store interface { AddMessage(message Message) (id string, err error) GetMessage(mailbox, id string) (Message, error) GetMessages(mailbox string) ([]Message, error) + MarkSeen(mailbox, id string) error PurgeMessages(mailbox string) error RemoveMessage(mailbox, id string) error VisitMailboxes(f func([]Message) (cont bool)) error @@ -43,6 +44,7 @@ type Message interface { Subject() string Source() (io.ReadCloser, error) Size() int64 + Seen() bool } // FromConfig creates an instance of the Store based on the provided configuration. diff --git a/pkg/test/storage_suite.go b/pkg/test/storage_suite.go index 9936145..b4135b0 100644 --- a/pkg/test/storage_suite.go +++ b/pkg/test/storage_suite.go @@ -28,6 +28,7 @@ func StoreSuite(t *testing.T, factory StoreFactory) { {"content", testContent, config.Storage{}}, {"delivery order", testDeliveryOrder, config.Storage{}}, {"size", testSize, config.Storage{}}, + {"seen", testSeen, config.Storage{}}, {"delete", testDelete, config.Storage{}}, {"purge", testPurge, config.Storage{}}, {"cap=10", testMsgCap, config.Storage{MailboxMsgCap: 10}}, @@ -65,6 +66,7 @@ func testMetadata(t *testing.T, store storage.Store) { To: to, Date: date, Subject: subject, + Seen: false, }, Reader: strings.NewReader(content), } @@ -107,6 +109,9 @@ func testMetadata(t *testing.T, store storage.Store) { if sm.Size() != int64(len(content)) { t.Errorf("got size %v, want: %v", sm.Size(), len(content)) } + if sm.Seen() { + t.Errorf("got seen %v, want: false", sm.Seen()) + } } // testContent generates some binary content and makes sure it is correctly retrieved. @@ -210,6 +215,42 @@ func testSize(t *testing.T, store storage.Store) { } } +// testSeen verifies a message can be marked as seen. +func testSeen(t *testing.T, store storage.Store) { + mailbox := "lisa" + id1, _ := DeliverToStore(t, store, mailbox, "whatever", time.Now()) + id2, _ := DeliverToStore(t, store, mailbox, "hello?", time.Now()) + // Confirm unseen. + msg, err := store.GetMessage(mailbox, id1) + if err != nil { + t.Fatal(err) + } + if msg.Seen() { + t.Errorf("got seen %v, want: false", msg.Seen()) + } + // Mark id1 seen. + err = store.MarkSeen(mailbox, id1) + if err != nil { + t.Fatal(err) + } + // Verify id1 seen. + msg, err = store.GetMessage(mailbox, id1) + if err != nil { + t.Fatal(err) + } + if !msg.Seen() { + t.Errorf("id1 got seen %v, want: true", msg.Seen()) + } + // Verify id2 still unseen. + msg, err = store.GetMessage(mailbox, id2) + if err != nil { + t.Fatal(err) + } + if msg.Seen() { + t.Errorf("id2 got seen %v, want: false", msg.Seen()) + } +} + // testDelete creates and deletes some messages. func testDelete(t *testing.T, store storage.Store) { mailbox := "fred"